Merge remote-tracking branch 'upstream/master' into sportage-2023-port

# Conflicts:
#	opendbc
#	panda
pull/26342/head^2
Jason Wen 3 years ago
commit cf873633cb
No known key found for this signature in database
GPG Key ID: 6AAEBEBBB33E2AD2
  1. 8
      .github/workflows/prebuilt.yaml
  2. 109
      .github/workflows/selfdrive_tests.yaml
  3. 8
      .github/workflows/setup/action.yaml
  4. 45
      .github/workflows/tools_tests.yaml
  5. 1
      .pre-commit-config.yaml
  6. 1
      SConstruct
  7. 2
      cereal
  8. 2
      opendbc
  9. 2
      panda
  10. 4
      release/build_devel.sh
  11. 68
      selfdrive/athena/tests/test_athenad.py
  12. 8
      selfdrive/car/car_helpers.py
  13. 14
      selfdrive/car/fw_versions.py
  14. 3
      selfdrive/car/gm/carstate.py
  15. 29
      selfdrive/car/hyundai/carcontroller.py
  16. 29
      selfdrive/car/hyundai/tests/test_hyundai.py
  17. 48
      selfdrive/car/hyundai/values.py
  18. 30
      selfdrive/controls/controlsd.py
  19. 3
      selfdrive/controls/tests/test_startup.py
  20. 2
      selfdrive/debug/check_timings.py
  21. 2
      selfdrive/loggerd/loggerd.cc
  22. 4
      selfdrive/modeld/models/dmonitoring_model.current
  23. 4
      selfdrive/modeld/models/dmonitoring_model.onnx
  24. 4
      selfdrive/modeld/models/dmonitoring_model_q.dlc
  25. 4
      selfdrive/modeld/models/supercombo.onnx
  26. 0
      selfdrive/modeld/tests/__init__.py
  27. 0
      selfdrive/modeld/tests/dmon_lag/repro.cc
  28. 0
      selfdrive/modeld/tests/snpe_benchmark/.gitignore
  29. 1
      selfdrive/modeld/tests/snpe_benchmark/benchmark.cc
  30. 0
      selfdrive/modeld/tests/snpe_benchmark/benchmark.sh
  31. 0
      selfdrive/modeld/tests/test_modeld.py
  32. 0
      selfdrive/modeld/tests/tf_test/build.sh
  33. 0
      selfdrive/modeld/tests/tf_test/main.cc
  34. 0
      selfdrive/modeld/tests/tf_test/pb_loader.py
  35. 0
      selfdrive/modeld/tests/timing/benchmark.py
  36. 21
      selfdrive/monitoring/driver_monitor.py
  37. 2
      selfdrive/test/process_replay/model_replay_ref_commit
  38. 2
      selfdrive/test/process_replay/ref_commit
  39. 23
      selfdrive/test/test_onroad.py
  40. 4
      selfdrive/ui/qt/offroad/driverview.cc
  41. 2
      selfdrive/ui/qt/offroad/driverview.h
  42. 2
      selfdrive/ui/qt/offroad/wifiManager.cc
  43. 55
      selfdrive/ui/qt/onroad.cc
  44. 7
      selfdrive/ui/qt/onroad.h
  45. 26
      selfdrive/ui/qt/widgets/cameraview.cc
  46. 6
      selfdrive/ui/qt/widgets/cameraview.h
  47. 2
      selfdrive/ui/translations/main_ar.ts
  48. 46
      selfdrive/ui/translations/main_ja.ts
  49. 50
      selfdrive/ui/translations/main_ko.ts
  50. 2
      selfdrive/ui/translations/main_nl.ts
  51. 2
      selfdrive/ui/translations/main_pl.ts
  52. 46
      selfdrive/ui/translations/main_pt-BR.ts
  53. 2
      selfdrive/ui/translations/main_th.ts
  54. 46
      selfdrive/ui/translations/main_zh-CHS.ts
  55. 46
      selfdrive/ui/translations/main_zh-CHT.ts
  56. 8
      selfdrive/ui/watch3.cc
  57. 1
      selfdrive/updated.py
  58. 2
      system/hardware/tici/test_power_draw.py
  59. 2
      tools/cabana/SConscript
  60. 184
      tools/cabana/binaryview.cc
  61. 72
      tools/cabana/binaryview.h
  62. 3
      tools/cabana/cabana.cc
  63. 5
      tools/cabana/canmessages.cc
  64. 2
      tools/cabana/canmessages.h
  65. 133
      tools/cabana/chartswidget.cc
  66. 27
      tools/cabana/chartswidget.h
  67. 34
      tools/cabana/dbcmanager.cc
  68. 8
      tools/cabana/dbcmanager.h
  69. 202
      tools/cabana/detailwidget.cc
  70. 46
      tools/cabana/detailwidget.h
  71. 61
      tools/cabana/historylog.cc
  72. 13
      tools/cabana/historylog.h
  73. 18
      tools/cabana/mainwin.cc
  74. 1
      tools/cabana/mainwin.h
  75. 73
      tools/cabana/messageswidget.cc
  76. 13
      tools/cabana/messageswidget.h
  77. 82
      tools/cabana/signaledit.cc
  78. 29
      tools/cabana/signaledit.h
  79. 25
      tools/cabana/videowidget.cc
  80. 5
      tools/cabana/videowidget.h

@ -2,7 +2,6 @@ name: prebuilt
on: on:
schedule: schedule:
- cron: '0 * * * *' - cron: '0 * * * *'
workflow_dispatch: workflow_dispatch:
env: env:
@ -11,9 +10,7 @@ env:
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
BUILD: | BUILD: |
docker pull $(grep -iohP '(?<=^from)\s+\S+' Dockerfile.openpilot_base) || true DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base .
docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true
docker build --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base .
jobs: jobs:
build_prebuilt: build_prebuilt:
@ -37,8 +34,7 @@ jobs:
- name: Build Docker image - name: Build Docker image
run: | run: |
eval "$BUILD" eval "$BUILD"
docker pull $DOCKER_REGISTRY/$IMAGE_NAME:latest || true DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$IMAGE_NAME:latest -t $DOCKER_REGISTRY/$IMAGE_NAME:latest -f Dockerfile.openpilot .
docker build --cache-from $DOCKER_REGISTRY/$IMAGE_NAME:latest -t $DOCKER_REGISTRY/$IMAGE_NAME:latest -f Dockerfile.openpilot .
- name: Push to container registry - name: Push to container registry
run: | run: |
$DOCKER_LOGIN $DOCKER_LOGIN

@ -18,15 +18,12 @@ env:
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
BUILD: | BUILD: |
docker pull $(grep -iohP '(?<=^from)\s+\S+' Dockerfile.openpilot_base) || true DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base .
docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true
docker build --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base .
RUN: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/sh -c RUN: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/sh -c
BUILD_CL: | BUILD_CL: |
docker pull $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest || true DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $CL_BASE_IMAGE:latest -f Dockerfile.openpilot_base_cl .
docker build --cache-from $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $CL_BASE_IMAGE:latest -f Dockerfile.openpilot_base_cl .
RUN_CL: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $CL_BASE_IMAGE /bin/sh -c RUN_CL: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $CL_BASE_IMAGE /bin/sh -c
UNIT_TEST: coverage run --append -m unittest discover UNIT_TEST: coverage run --append -m unittest discover
@ -41,13 +38,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 0
submodules: true submodules: true
- name: Build devel - name: Build devel
run: TARGET_DIR=$STRIPPED_DIR release/build_devel.sh run: TARGET_DIR=$STRIPPED_DIR release/build_devel.sh
- uses: ./.github/workflows/setup - uses: ./.github/workflows/setup
with:
save-cache: true
- name: Check submodules - name: Check submodules
if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot' if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot'
run: release/check-submodules.sh run: release/check-submodules.sh
@ -72,18 +66,20 @@ jobs:
build_all: build_all:
name: build all name: build all
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 50 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- uses: ./.github/workflows/setup - uses: ./.github/workflows/setup
with:
save-cache: true
- name: Build openpilot with all flags - name: Build openpilot with all flags
run: ${{ env.RUN }} "scons -j$(nproc) --extras && release/check-dirty.sh" run: ${{ env.RUN }} "scons -j$(nproc) --extras && release/check-dirty.sh"
- name: Cleanup scons cache - name: Cleanup scons cache
run: | run: |
${{ env.RUN }} "rm -rf /tmp/scons_cache/* && \ ${{ env.RUN }} "rm -rf /tmp/scons_cache/* && \
scons -j$(nproc) --extras --cache-populate" scons -j$(nproc) --cache-populate"
#build_mac: #build_mac:
# name: build macos # name: build macos
@ -145,7 +141,7 @@ jobs:
docker_push: docker_push:
name: docker push name: docker push
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 50 timeout-minutes: 22
if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot' if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot'
needs: static_analysis # hack to ensure slow tests run first since this and static_analysis are fast needs: static_analysis # hack to ensure slow tests run first since this and static_analysis are fast
steps: steps:
@ -154,12 +150,14 @@ jobs:
submodules: true submodules: true
- name: Build Docker image - name: Build Docker image
run: eval "$BUILD" run: eval "$BUILD"
timeout-minutes: 13
- name: Push to container registry - name: Push to container registry
run: | run: |
$DOCKER_LOGIN $DOCKER_LOGIN
docker push $DOCKER_REGISTRY/$BASE_IMAGE:latest docker push $DOCKER_REGISTRY/$BASE_IMAGE:latest
- name: Build CL Docker image - name: Build CL Docker image
run: eval "$BUILD_CL" run: eval "$BUILD_CL"
timeout-minutes: 4
- name: Push to container registry - name: Push to container registry
run: | run: |
$DOCKER_LOGIN $DOCKER_LOGIN
@ -168,7 +166,7 @@ jobs:
static_analysis: static_analysis:
name: static analysis name: static analysis
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 50 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
@ -176,21 +174,23 @@ jobs:
- name: Build Docker image - name: Build Docker image
run: eval "$BUILD" run: eval "$BUILD"
- name: pre-commit - name: pre-commit
timeout-minutes: 5
run: ${{ env.RUN }} "pre-commit run --all" run: ${{ env.RUN }} "pre-commit run --all"
valgrind: valgrind:
name: valgrind name: valgrind
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 50 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- uses: ./.github/workflows/setup - uses: ./.github/workflows/setup
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Run valgrind - name: Run valgrind
run: | run: |
${{ env.RUN }} "scons -j$(nproc) && \ ${{ env.RUN }} "python selfdrive/test/test_valgrind_replay.py"
python selfdrive/test/test_valgrind_replay.py"
- name: Print logs - name: Print logs
if: always() if: always()
run: cat selfdrive/test/valgrind_logs.txt run: cat selfdrive/test/valgrind_logs.txt
@ -198,16 +198,18 @@ jobs:
unit_tests: unit_tests:
name: unit tests name: unit tests
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 50 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- uses: ./.github/workflows/setup - uses: ./.github/workflows/setup
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Run unit tests - name: Run unit tests
timeout-minutes: 15
run: | run: |
${{ env.RUN }} "export SKIP_LONG_TESTS=1 && \ ${{ env.RUN }} "export SKIP_LONG_TESTS=1 && \
scons -j$(nproc) && \
$UNIT_TEST common && \ $UNIT_TEST common && \
$UNIT_TEST opendbc/can && \ $UNIT_TEST opendbc/can && \
$UNIT_TEST selfdrive/boardd && \ $UNIT_TEST selfdrive/boardd && \
@ -220,7 +222,6 @@ jobs:
$UNIT_TEST selfdrive/athena && \ $UNIT_TEST selfdrive/athena && \
$UNIT_TEST selfdrive/thermald && \ $UNIT_TEST selfdrive/thermald && \
$UNIT_TEST system/hardware/tici && \ $UNIT_TEST system/hardware/tici && \
$UNIT_TEST selfdrive/modeld && \
$UNIT_TEST tools/lib/tests && \ $UNIT_TEST tools/lib/tests && \
./selfdrive/ui/tests/create_test_translations.sh && \ ./selfdrive/ui/tests/create_test_translations.sh && \
QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \ QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \
@ -239,7 +240,7 @@ jobs:
process_replay: process_replay:
name: process replay name: process replay
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 50 timeout-minutes: 25
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
@ -251,10 +252,12 @@ jobs:
with: with:
path: /tmp/comma_download_cache path: /tmp/comma_download_cache
key: proc-replay-${{ hashFiles('.github/workflows/selfdrive_tests.yaml', 'selfdrive/test/process_replay/ref_commit') }} key: proc-replay-${{ hashFiles('.github/workflows/selfdrive_tests.yaml', 'selfdrive/test/process_replay/ref_commit') }}
- name: Build openpilot
run: |
${{ env.RUN }} "scons -j$(nproc)"
- name: Run replay - name: Run replay
run: | run: |
${{ env.RUN }} "scons -j$(nproc) && \ ${{ env.RUN }} "CI=1 coverage run selfdrive/test/process_replay/test_processes.py -j$(nproc) && \
CI=1 coverage run selfdrive/test/process_replay/test_processes.py -j$(nproc) && \
coverage xml" coverage xml"
- name: Print diff - name: Print diff
if: always() if: always()
@ -268,15 +271,14 @@ jobs:
- name: Upload reference logs - name: Upload reference logs
if: ${{ failure() && github.event_name == 'pull_request' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }} if: ${{ failure() && github.event_name == 'pull_request' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }}
run: | run: |
${{ env.RUN }} "scons -j$(nproc) && \ ${{ env.RUN }} "CI=1 AZURE_TOKEN='$AZURE_TOKEN' python selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
CI=1 AZURE_TOKEN='$AZURE_TOKEN' python selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
- name: "Upload coverage to Codecov" - name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v2
model_replay_onnx: test_modeld:
name: model replay onnx name: model tests
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 50 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
@ -285,29 +287,41 @@ jobs:
- name: Build Docker image - name: Build Docker image
# Sim docker is needed to get the OpenCL drivers # Sim docker is needed to get the OpenCL drivers
run: eval "$BUILD_CL" run: eval "$BUILD_CL"
- name: Run replay - name: Build openpilot
run: |
${{ env.RUN }} "scons -j$(nproc)"
- name: Run model replay with ONNX
timeout-minutes: 2
run: |
${{ env.RUN_CL }} "ONNXCPU=1 CI=1 coverage run selfdrive/test/process_replay/model_replay.py && \
coverage xml"
- name: Run unit tests
timeout-minutes: 5
run: | run: |
${{ env.RUN_CL }} "scons -j$(nproc) && \ ${{ env.RUN_CL }} "$UNIT_TEST selfdrive/modeld && \
ONNXCPU=1 CI=1 coverage run \
selfdrive/test/process_replay/model_replay.py -j$(nproc) && \
coverage xml" coverage xml"
- name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v2
test_longitudinal: test_longitudinal:
name: longitudinal name: longitudinal
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 50 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- uses: ./.github/workflows/setup - uses: ./.github/workflows/setup
- name: Build openpilot
run: |
${{ env.RUN }} "scons -j$(nproc)"
- name: Test longitudinal - name: Test longitudinal
run: | run: |
${{ env.RUN }} "mkdir -p selfdrive/test/out && \ ${{ env.RUN }} "mkdir -p selfdrive/test/out && \
scons -j$(nproc) && \
cd selfdrive/test/longitudinal_maneuvers && \ cd selfdrive/test/longitudinal_maneuvers && \
coverage run ./test_longitudinal.py && \ coverage run ./test_longitudinal.py && \
coverage xml" coverage xml"
timeout-minutes: 2
- name: "Upload coverage to Codecov" - name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v2
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
@ -320,7 +334,7 @@ jobs:
test_cars: test_cars:
name: cars name: cars
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 50 timeout-minutes: 20
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -336,10 +350,12 @@ jobs:
with: with:
path: /tmp/comma_download_cache path: /tmp/comma_download_cache
key: car_models-${{ hashFiles('selfdrive/car/tests/test_models.py', 'selfdrive/car/tests/routes.py') }}-${{ matrix.job }} key: car_models-${{ hashFiles('selfdrive/car/tests/test_models.py', 'selfdrive/car/tests/routes.py') }}-${{ matrix.job }}
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Test car models - name: Test car models
timeout-minutes: 12
run: | run: |
${{ env.RUN }} "scons -j$(nproc) && \ ${{ env.RUN }} "coverage run -m pytest selfdrive/car/tests/test_models.py && \
coverage run -m pytest selfdrive/car/tests/test_models.py && \
coverage xml && \ coverage xml && \
chmod -R 777 /tmp/comma_download_cache" chmod -R 777 /tmp/comma_download_cache"
env: env:
@ -348,29 +364,10 @@ jobs:
- name: "Upload coverage to Codecov" - name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v2
docs:
name: build docs
runs-on: ubuntu-20.04
timeout-minutes: 50
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Build docker container
run: |
docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true
docker pull $DOCKER_REGISTRY/openpilot-docs:latest || true
DOCKER_BUILDKIT=1 docker build --cache-from $DOCKER_REGISTRY/openpilot-docs:latest -t $DOCKER_REGISTRY/openpilot-docs:latest -f docs/docker/Dockerfile .
- name: Push docker container
if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot'
run: |
$DOCKER_LOGIN
docker push $DOCKER_REGISTRY/openpilot-docs:latest
car_docs_diff: car_docs_diff:
name: comment on PR with car docs diff name: PR comments
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 50 timeout-minutes: 20
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

@ -15,9 +15,9 @@ runs:
# build cache # build cache
- id: date - id: date
shell: bash shell: bash
run: echo "::set-output name=date::$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d')" run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV
- shell: bash - shell: bash
run: echo "${{ steps.date.outputs.date }}" run: echo "$CACHE_COMMIT_DATE"
- shell: bash - shell: bash
run: echo "CACHE_SKIP_SAVE=true" >> $GITHUB_ENV run: echo "CACHE_SKIP_SAVE=true" >> $GITHUB_ENV
if: github.ref != 'refs/heads/master' || inputs.save-cache == 'false' if: github.ref != 'refs/heads/master' || inputs.save-cache == 'false'
@ -27,9 +27,9 @@ runs:
uses: actions/cache@03e00da99d75a2204924908e1cca7902cafce66b uses: actions/cache@03e00da99d75a2204924908e1cca7902cafce66b
with: with:
path: /tmp/scons_cache path: /tmp/scons_cache
key: scons-${{ steps.date.outputs.date }}-${{ github.sha }} key: scons-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
restore-keys: | restore-keys: |
scons-${{ steps.date.outputs.date }}- scons-${{ env.CACHE_COMMIT_DATE }}-
scons- scons-
# build our docker image # build our docker image

@ -2,6 +2,8 @@ name: tools
on: on:
push: push:
branches-ignore:
- 'testing-closet*'
pull_request: pull_request:
concurrency: concurrency:
@ -15,21 +17,20 @@ env:
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
BUILD: | BUILD: |
docker pull $(grep -iohP '(?<=^from)\s+\S+' Dockerfile.openpilot_base) || true DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base .
docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true
docker build --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base . RUN: docker run --shm-size 1G -v $GITHUB_WORKSPACE:/tmp/openpilot -w /tmp/openpilot -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/sh -c
BUILD_CL: | BUILD_CL: |
docker pull $(grep -iohP '(?<=^from)\s+\S+' Dockerfile.openpilot_base_cl) || true DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $CL_BASE_IMAGE:latest -f Dockerfile.openpilot_base_cl .
docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true RUN_CL: docker run --shm-size 1G -v $GITHUB_WORKSPACE:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $CL_BASE_IMAGE /bin/sh -c
docker build --cache-from $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $CL_BASE_IMAGE:latest -f Dockerfile.openpilot_base_cl .
RUN: docker run --shm-size 1G -v $PWD:/tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e \
GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/comma_download_cache:/tmp/comma_download_cache $BASE_IMAGE /bin/sh -c
jobs: jobs:
plotjuggler: plotjuggler:
name: plotjuggler name: plotjuggler
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 30 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
@ -37,6 +38,7 @@ jobs:
- name: Build Docker image - name: Build Docker image
run: eval "$BUILD" run: eval "$BUILD"
- name: Unit test - name: Unit test
timeout-minutes: 2
run: | run: |
${{ env.RUN }} "scons -j$(nproc) --directory=/tmp/openpilot/cereal && \ ${{ env.RUN }} "scons -j$(nproc) --directory=/tmp/openpilot/cereal && \
apt-get update && \ apt-get update && \
@ -47,7 +49,7 @@ jobs:
simulator: simulator:
name: simulator name: simulator
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 50 timeout-minutes: 30
env: env:
IMAGE_NAME: openpilot-sim IMAGE_NAME: openpilot-sim
if: github.repository == 'commaai/openpilot' if: github.repository == 'commaai/openpilot'
@ -61,12 +63,29 @@ jobs:
run: eval "$BUILD" run: eval "$BUILD"
- name: Build base cl image - name: Build base cl image
run: eval "$BUILD_CL" run: eval "$BUILD_CL"
- name: Pull latest simulator image
run: docker pull $DOCKER_REGISTRY/$IMAGE_NAME:latest || true
- name: Build simulator image - name: Build simulator image
run: docker build --cache-from $DOCKER_REGISTRY/$IMAGE_NAME:latest -t $DOCKER_REGISTRY/$IMAGE_NAME:latest -f tools/sim/Dockerfile.sim . run: DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/$IMAGE_NAME:latest -t $DOCKER_REGISTRY/$IMAGE_NAME:latest -f tools/sim/Dockerfile.sim .
- name: Push to container registry - name: Push to container registry
if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot' if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot'
run: | run: |
$DOCKER_LOGIN $DOCKER_LOGIN
docker push $DOCKER_REGISTRY/$IMAGE_NAME:latest docker push $DOCKER_REGISTRY/$IMAGE_NAME:latest
docs:
name: build docs
runs-on: ubuntu-20.04
timeout-minutes: 25
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Build docker container
run: |
DOCKER_BUILDKIT=1 docker build --pull --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $DOCKER_REGISTRY/openpilot-docs:latest -t $DOCKER_REGISTRY/openpilot-docs:latest -f docs/docker/Dockerfile .
- name: Push docker container
if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot'
run: |
$DOCKER_LOGIN
docker push $DOCKER_REGISTRY/openpilot-docs:latest

@ -53,6 +53,7 @@ repos:
types: [python] types: [python]
exclude: '^(pyextra/)|(cereal/)|(rednose/)|(panda/)|(laika/)|(laika_repo/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)' exclude: '^(pyextra/)|(cereal/)|(rednose/)|(panda/)|(laika/)|(laika_repo/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)'
args: args:
- -j0
- -rn - -rn
- -sn - -sn
- --rcfile=.pylintrc - --rcfile=.pylintrc

@ -327,6 +327,7 @@ qt_flags = [
qt_env['CXXFLAGS'] += qt_flags qt_env['CXXFLAGS'] += qt_flags
qt_env['LIBPATH'] += ['#selfdrive/ui'] qt_env['LIBPATH'] += ['#selfdrive/ui']
qt_env['LIBS'] = qt_libs qt_env['LIBS'] = qt_libs
qt_env['QT_MOCHPREFIX'] = cache_dir + '/moc_files/moc_'
if GetOption("clazy"): if GetOption("clazy"):
checks = [ checks = [

@ -1 +1 @@
Subproject commit 5766e645f2ee2a131b145fb1ea9e3b7c55a4a740 Subproject commit 107048c83ec2f488286a1be314e7aece0a20a6b1

@ -1 +1 @@
Subproject commit 437df00bc8bc4f522a3828cbe691212ccc7ba9c6 Subproject commit 8f245e6ef5e25814d8e6e1f096221f6dfeefe86b

@ -1 +1 @@
Subproject commit 87b01b89068375e89c4eaf9d155ff3dc0651006d Subproject commit b5362b9c2770b09552d5533cf776b4c04b09b464

@ -22,8 +22,8 @@ pre-commit uninstall || true
echo "[-] bringing master-ci and devel in sync T=$SECONDS" echo "[-] bringing master-ci and devel in sync T=$SECONDS"
cd $TARGET_DIR cd $TARGET_DIR
git fetch origin master-ci git fetch --depth 1 origin master-ci
git fetch origin devel git fetch --depth 1 origin devel
git checkout -f --track origin/master-ci git checkout -f --track origin/master-ci
git reset --hard master-ci git reset --hard master-ci

@ -9,6 +9,7 @@ import threading
import queue import queue
import unittest import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional
from multiprocessing import Process from multiprocessing import Process
from pathlib import Path from pathlib import Path
@ -46,12 +47,26 @@ class TestAthenadMethods(unittest.TestCase):
else: else:
os.unlink(p) os.unlink(p)
def wait_for_upload(self):
# *** test helpers ***
@staticmethod
def _wait_for_upload():
now = time.time() now = time.time()
while time.time() - now < 5: while time.time() - now < 5:
if athenad.upload_queue.qsize() == 0: if athenad.upload_queue.qsize() == 0:
break break
@staticmethod
def _create_file(file: str, parent: Optional[str] = None) -> str:
fn = os.path.join(athenad.ROOT if parent is None else parent, file)
os.makedirs(os.path.dirname(fn), exist_ok=True)
Path(fn).touch()
return fn
# *** test cases ***
def test_echo(self): def test_echo(self):
assert dispatcher["echo"]("bob") == "bob" assert dispatcher["echo"]("bob") == "bob"
@ -85,9 +100,7 @@ class TestAthenadMethods(unittest.TestCase):
filenames = ['qlog', 'qcamera.ts', 'rlog', 'fcamera.hevc', 'ecamera.hevc', 'dcamera.hevc'] filenames = ['qlog', 'qcamera.ts', 'rlog', 'fcamera.hevc', 'ecamera.hevc', 'dcamera.hevc']
files = [f'{route}--{s}/{f}' for s in segments for f in filenames] files = [f'{route}--{s}/{f}' for s in segments for f in filenames]
for file in files: for file in files:
fn = os.path.join(athenad.ROOT, file) self._create_file(file)
os.makedirs(os.path.dirname(fn), exist_ok=True)
Path(fn).touch()
resp = dispatcher["listDataDirectory"]() resp = dispatcher["listDataDirectory"]()
self.assertTrue(resp, 'list empty!') self.assertTrue(resp, 'list empty!')
@ -121,16 +134,14 @@ class TestAthenadMethods(unittest.TestCase):
self.assertCountEqual(resp, expected) self.assertCountEqual(resp, expected)
def test_strip_bz2_extension(self): def test_strip_bz2_extension(self):
fn = os.path.join(athenad.ROOT, 'qlog.bz2') fn = self._create_file('qlog.bz2')
Path(fn).touch()
if fn.endswith('.bz2'): if fn.endswith('.bz2'):
self.assertEqual(athenad.strip_bz2_extension(fn), fn[:-4]) self.assertEqual(athenad.strip_bz2_extension(fn), fn[:-4])
@with_http_server @with_http_server
def test_do_upload(self, host): def test_do_upload(self, host):
fn = os.path.join(athenad.ROOT, 'qlog.bz2') fn = self._create_file('qlog.bz2')
Path(fn).touch()
item = athenad.UploadItem(path=fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='') item = athenad.UploadItem(path=fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='')
with self.assertRaises(requests.exceptions.ConnectionError): with self.assertRaises(requests.exceptions.ConnectionError):
@ -142,8 +153,7 @@ class TestAthenadMethods(unittest.TestCase):
@with_http_server @with_http_server
def test_uploadFileToUrl(self, host): def test_uploadFileToUrl(self, host):
fn = os.path.join(athenad.ROOT, 'qlog.bz2') fn = self._create_file('qlog.bz2')
Path(fn).touch()
resp = dispatcher["uploadFileToUrl"]("qlog.bz2", f"{host}/qlog.bz2", {}) resp = dispatcher["uploadFileToUrl"]("qlog.bz2", f"{host}/qlog.bz2", {})
self.assertEqual(resp['enqueued'], 1) self.assertEqual(resp['enqueued'], 1)
@ -154,8 +164,7 @@ class TestAthenadMethods(unittest.TestCase):
@with_http_server @with_http_server
def test_uploadFileToUrl_duplicate(self, host): def test_uploadFileToUrl_duplicate(self, host):
fn = os.path.join(athenad.ROOT, 'qlog.bz2') self._create_file('qlog.bz2')
Path(fn).touch()
url1 = f"{host}/qlog.bz2?sig=sig1" url1 = f"{host}/qlog.bz2?sig=sig1"
dispatcher["uploadFileToUrl"]("qlog.bz2", url1, {}) dispatcher["uploadFileToUrl"]("qlog.bz2", url1, {})
@ -172,8 +181,7 @@ class TestAthenadMethods(unittest.TestCase):
@with_http_server @with_http_server
def test_upload_handler(self, host): def test_upload_handler(self, host):
fn = os.path.join(athenad.ROOT, 'qlog.bz2') fn = self._create_file('qlog.bz2')
Path(fn).touch()
item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True)
end_event = threading.Event() end_event = threading.Event()
@ -182,7 +190,7 @@ class TestAthenadMethods(unittest.TestCase):
athenad.upload_queue.put_nowait(item) athenad.upload_queue.put_nowait(item)
try: try:
self.wait_for_upload() self._wait_for_upload()
time.sleep(0.1) time.sleep(0.1)
# TODO: verify that upload actually succeeded # TODO: verify that upload actually succeeded
@ -195,8 +203,7 @@ class TestAthenadMethods(unittest.TestCase):
def test_upload_handler_retry(self, host, mock_put): def test_upload_handler_retry(self, host, mock_put):
for status, retry in ((500, True), (412, False)): for status, retry in ((500, True), (412, False)):
mock_put.return_value.status_code = status mock_put.return_value.status_code = status
fn = os.path.join(athenad.ROOT, 'qlog.bz2') fn = self._create_file('qlog.bz2')
Path(fn).touch()
item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True)
end_event = threading.Event() end_event = threading.Event()
@ -205,7 +212,7 @@ class TestAthenadMethods(unittest.TestCase):
athenad.upload_queue.put_nowait(item) athenad.upload_queue.put_nowait(item)
try: try:
self.wait_for_upload() self._wait_for_upload()
time.sleep(0.1) time.sleep(0.1)
self.assertEqual(athenad.upload_queue.qsize(), 1 if retry else 0) self.assertEqual(athenad.upload_queue.qsize(), 1 if retry else 0)
@ -217,8 +224,7 @@ class TestAthenadMethods(unittest.TestCase):
def test_upload_handler_timeout(self): def test_upload_handler_timeout(self):
"""When an upload times out or fails to connect it should be placed back in the queue""" """When an upload times out or fails to connect it should be placed back in the queue"""
fn = os.path.join(athenad.ROOT, 'qlog.bz2') fn = self._create_file('qlog.bz2')
Path(fn).touch()
item = athenad.UploadItem(path=fn, url="http://localhost:44444/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) item = athenad.UploadItem(path=fn, url="http://localhost:44444/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True)
item_no_retry = item._replace(retry_count=MAX_RETRY_COUNT) item_no_retry = item._replace(retry_count=MAX_RETRY_COUNT)
@ -228,14 +234,14 @@ class TestAthenadMethods(unittest.TestCase):
try: try:
athenad.upload_queue.put_nowait(item_no_retry) athenad.upload_queue.put_nowait(item_no_retry)
self.wait_for_upload() self._wait_for_upload()
time.sleep(0.1) time.sleep(0.1)
# Check that upload with retry count exceeded is not put back # Check that upload with retry count exceeded is not put back
self.assertEqual(athenad.upload_queue.qsize(), 0) self.assertEqual(athenad.upload_queue.qsize(), 0)
athenad.upload_queue.put_nowait(item) athenad.upload_queue.put_nowait(item)
self.wait_for_upload() self._wait_for_upload()
time.sleep(0.1) time.sleep(0.1)
# Check that upload item was put back in the queue with incremented retry count # Check that upload item was put back in the queue with incremented retry count
@ -256,7 +262,7 @@ class TestAthenadMethods(unittest.TestCase):
thread = threading.Thread(target=athenad.upload_handler, args=(end_event,)) thread = threading.Thread(target=athenad.upload_handler, args=(end_event,))
thread.start() thread.start()
try: try:
self.wait_for_upload() self._wait_for_upload()
time.sleep(0.1) time.sleep(0.1)
self.assertEqual(athenad.upload_queue.qsize(), 0) self.assertEqual(athenad.upload_queue.qsize(), 0)
@ -269,8 +275,7 @@ class TestAthenadMethods(unittest.TestCase):
ts = int(t_future.strftime("%s")) * 1000 ts = int(t_future.strftime("%s")) * 1000
# Item that would time out if actually uploaded # Item that would time out if actually uploaded
fn = os.path.join(athenad.ROOT, 'qlog.bz2') fn = self._create_file('qlog.bz2')
Path(fn).touch()
item = athenad.UploadItem(path=fn, url="http://localhost:44444/qlog.bz2", headers={}, created_at=ts, id='', allow_cellular=True) item = athenad.UploadItem(path=fn, url="http://localhost:44444/qlog.bz2", headers={}, created_at=ts, id='', allow_cellular=True)
@ -279,7 +284,7 @@ class TestAthenadMethods(unittest.TestCase):
thread.start() thread.start()
try: try:
athenad.upload_queue.put_nowait(item) athenad.upload_queue.put_nowait(item)
self.wait_for_upload() self._wait_for_upload()
time.sleep(0.1) time.sleep(0.1)
self.assertEqual(athenad.upload_queue.qsize(), 0) self.assertEqual(athenad.upload_queue.qsize(), 0)
@ -292,8 +297,7 @@ class TestAthenadMethods(unittest.TestCase):
@with_http_server @with_http_server
def test_listUploadQueueCurrent(self, host): def test_listUploadQueueCurrent(self, host):
fn = os.path.join(athenad.ROOT, 'qlog.bz2') fn = self._create_file('qlog.bz2')
Path(fn).touch()
item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True)
end_event = threading.Event() end_event = threading.Event()
@ -302,7 +306,7 @@ class TestAthenadMethods(unittest.TestCase):
try: try:
athenad.upload_queue.put_nowait(item) athenad.upload_queue.put_nowait(item)
self.wait_for_upload() self._wait_for_upload()
items = dispatcher["listUploadQueue"]() items = dispatcher["listUploadQueue"]()
self.assertEqual(len(items), 1) self.assertEqual(len(items), 1)
@ -405,9 +409,9 @@ class TestAthenadMethods(unittest.TestCase):
def test_get_logs_to_send_sorted(self): def test_get_logs_to_send_sorted(self):
fl = list() fl = list()
for i in range(10): for i in range(10):
fn = os.path.join(swaglog.SWAGLOG_DIR, f'swaglog.{i:010}') file = f'swaglog.{i:010}'
Path(fn).touch() self._create_file(file, athenad.SWAGLOG_DIR)
fl.append(os.path.basename(fn)) fl.append(file)
# ensure the list is all logs except most recent # ensure the list is all logs except most recent
sl = athenad.get_logs_to_send_sorted() sl = athenad.get_logs_to_send_sorted()

@ -76,7 +76,7 @@ interfaces = load_interfaces(interface_names)
# **** for use live only **** # **** for use live only ****
def fingerprint(logcan, sendcan): def fingerprint(logcan, sendcan, num_pandas):
fixed_fingerprint = os.environ.get('FINGERPRINT', "") fixed_fingerprint = os.environ.get('FINGERPRINT', "")
skip_fw_query = os.environ.get('SKIP_FW_QUERY', False) skip_fw_query = os.environ.get('SKIP_FW_QUERY', False)
ecu_rx_addrs = set() ecu_rx_addrs = set()
@ -100,7 +100,7 @@ def fingerprint(logcan, sendcan):
cloudlog.warning("Getting VIN & FW versions") cloudlog.warning("Getting VIN & FW versions")
vin_rx_addr, vin = get_vin(logcan, sendcan, bus) vin_rx_addr, vin = get_vin(logcan, sendcan, bus)
ecu_rx_addrs = get_present_ecus(logcan, sendcan) ecu_rx_addrs = get_present_ecus(logcan, sendcan)
car_fw = get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs) car_fw = get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, num_pandas=num_pandas)
cached = False cached = False
exact_fw_match, fw_candidates = match_fw_to_car(car_fw) exact_fw_match, fw_candidates = match_fw_to_car(car_fw)
@ -173,8 +173,8 @@ def fingerprint(logcan, sendcan):
return car_fingerprint, finger, vin, car_fw, source, exact_match return car_fingerprint, finger, vin, car_fw, source, exact_match
def get_car(logcan, sendcan): def get_car(logcan, sendcan, num_pandas=1):
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(logcan, sendcan) candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(logcan, sendcan, num_pandas)
if candidate is None: if candidate is None:
cloudlog.warning("car doesn't match any fingerprints: %r", fingerprints) cloudlog.warning("car doesn't match any fingerprints: %r", fingerprints)

@ -194,14 +194,14 @@ def get_brand_ecu_matches(ecu_rx_addrs):
return brand_matches return brand_matches
def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, debug=False, progress=False): def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pandas=1, debug=False, progress=False):
"""Queries for FW versions ordering brands by likelihood, breaks when exact match is found""" """Queries for FW versions ordering brands by likelihood, breaks when exact match is found"""
all_car_fw = [] all_car_fw = []
brand_matches = get_brand_ecu_matches(ecu_rx_addrs) brand_matches = get_brand_ecu_matches(ecu_rx_addrs)
for brand in sorted(brand_matches, key=lambda b: len(brand_matches[b]), reverse=True): for brand in sorted(brand_matches, key=lambda b: len(brand_matches[b]), reverse=True):
car_fw = get_fw_versions(logcan, sendcan, query_brand=brand, timeout=timeout, debug=debug, progress=progress) 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) all_car_fw.extend(car_fw)
# Try to match using FW returned from this brand only # Try to match using FW returned from this brand only
matches = match_fw_to_car_exact(build_fw_dict(car_fw)) matches = match_fw_to_car_exact(build_fw_dict(car_fw))
@ -211,7 +211,7 @@ def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, debug=Fa
return all_car_fw return all_car_fw
def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, 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() versions = VERSIONS.copy()
# Each brand can define extra ECUs to query for data collection # Each brand can define extra ECUs to query for data collection
@ -252,6 +252,10 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1,
for addr in tqdm(addrs, disable=not progress): for addr in tqdm(addrs, disable=not progress):
for addr_chunk in chunks(addr): for addr_chunk in chunks(addr):
for brand, r in requests: for brand, r in requests:
# Skip query if no panda available
if r.bus > num_pandas * 4 - 1:
continue
try: try:
addrs = [(a, s) for (b, a, s) in addr_chunk if b in (brand, 'any') and addrs = [(a, s) for (b, a, s) in addr_chunk if b in (brand, 'any') and
(len(r.whitelist_ecus) == 0 or ecu_types[(b, a, s)] in r.whitelist_ecus)] (len(r.whitelist_ecus) == 0 or ecu_types[(b, a, s)] in r.whitelist_ecus)]
@ -292,6 +296,7 @@ if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
logcan = messaging.sub_sock('can') logcan = messaging.sub_sock('can')
pandaStates_sock = messaging.sub_sock('pandaStates')
sendcan = messaging.pub_sock('sendcan') sendcan = messaging.pub_sock('sendcan')
extra: Any = None extra: Any = None
@ -305,6 +310,7 @@ if __name__ == "__main__":
extra = {"any": {"debug": extra}} extra = {"any": {"debug": extra}}
time.sleep(1.) time.sleep(1.)
num_pandas = len(messaging.recv_one_retry(pandaStates_sock).pandaStates)
t = time.time() t = time.time()
print("Getting vin...") print("Getting vin...")
@ -314,7 +320,7 @@ if __name__ == "__main__":
print() print()
t = time.time() t = time.time()
fw_vers = get_fw_versions(logcan, sendcan, query_brand=args.brand, extra=extra, debug=args.debug, progress=True) fw_vers = get_fw_versions(logcan, sendcan, query_brand=args.brand, extra=extra, num_pandas=num_pandas, debug=args.debug, progress=True)
_, candidates = match_fw_to_car(fw_vers) _, candidates = match_fw_to_car(fw_vers)
print() print()

@ -96,9 +96,7 @@ class CarState(CarStateBase):
ret.cruiseState.standstill = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL ret.cruiseState.standstill = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL
if self.CP.networkLocation == NetworkLocation.fwdCamera: if self.CP.networkLocation == NetworkLocation.fwdCamera:
ret.cruiseState.speed = cam_cp.vl["ASCMActiveCruiseControlStatus"]["ACCSpeedSetpoint"] * CV.KPH_TO_MS ret.cruiseState.speed = cam_cp.vl["ASCMActiveCruiseControlStatus"]["ACCSpeedSetpoint"] * CV.KPH_TO_MS
ret.stockAeb = cam_cp.vl["AEBCmd"]["AEBCmdActive"] != 0 ret.stockAeb = cam_cp.vl["AEBCmd"]["AEBCmdActive"] != 0
ret.stockFcw = cam_cp.vl["ASCMActiveCruiseControlStatus"]["FCWAlert"] != 0
return ret return ret
@ -110,7 +108,6 @@ class CarState(CarStateBase):
signals += [ signals += [
("AEBCmdActive", "AEBCmd"), ("AEBCmdActive", "AEBCmd"),
("RollingCounter", "ASCMLKASteeringCmd"), ("RollingCounter", "ASCMLKASteeringCmd"),
("FCWAlert", "ASCMActiveCruiseControlStatus"),
("ACCSpeedSetpoint", "ASCMActiveCruiseControlStatus"), ("ACCSpeedSetpoint", "ASCMActiveCruiseControlStatus"),
] ]
checks += [ checks += [

@ -90,13 +90,27 @@ class CarController:
addr, bus = 0x730, 5 addr, bus = 0x730, 5
can_sends.append([addr, 0, b"\x02\x3E\x80\x00\x00\x00\x00\x00", bus]) can_sends.append([addr, 0, b"\x02\x3E\x80\x00\x00\x00\x00\x00", bus])
# >90 degree steering fault prevention
# Count up to MAX_ANGLE_FRAMES, at which point we need to cut torque to avoid a steering fault
if CC.latActive and abs(CS.out.steeringAngleDeg) >= MAX_ANGLE:
self.angle_limit_counter += 1
else:
self.angle_limit_counter = 0
# Cut steer actuation bit for two frames and hold torque with induced temporary fault
torque_fault = CC.latActive and self.angle_limit_counter > MAX_ANGLE_FRAMES
lat_active = CC.latActive and not torque_fault
if self.angle_limit_counter >= MAX_ANGLE_FRAMES + MAX_ANGLE_CONSECUTIVE_FRAMES:
self.angle_limit_counter = 0
# CAN-FD platforms # CAN-FD platforms
if self.CP.carFingerprint in CANFD_CAR: if self.CP.carFingerprint in CANFD_CAR:
hda2 = self.CP.flags & HyundaiFlags.CANFD_HDA2 hda2 = self.CP.flags & HyundaiFlags.CANFD_HDA2
hda2_long = hda2 and self.CP.openpilotLongitudinalControl hda2_long = hda2 and self.CP.openpilotLongitudinalControl
# steering control # steering control
can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, CC.enabled, CC.latActive, apply_steer)) can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, CC.enabled, lat_active, apply_steer))
# disable LFA on HDA2 # disable LFA on HDA2
if self.frame % 5 == 0 and hda2: if self.frame % 5 == 0 and hda2:
@ -132,19 +146,6 @@ class CarController:
can_sends.append(hyundaicanfd.create_buttons(self.packer, CS.buttons_counter+1, Buttons.RES_ACCEL)) can_sends.append(hyundaicanfd.create_buttons(self.packer, CS.buttons_counter+1, Buttons.RES_ACCEL))
self.last_button_frame = self.frame self.last_button_frame = self.frame
else: else:
# Count up to MAX_ANGLE_FRAMES, at which point we need to cut torque to avoid a steering fault
if CC.latActive and abs(CS.out.steeringAngleDeg) >= MAX_ANGLE:
self.angle_limit_counter += 1
else:
self.angle_limit_counter = 0
# Cut steer actuation bit for two frames and hold torque with induced temporary fault
torque_fault = CC.latActive and self.angle_limit_counter > MAX_ANGLE_FRAMES
lat_active = CC.latActive and not torque_fault
if self.angle_limit_counter >= MAX_ANGLE_FRAMES + MAX_ANGLE_CONSECUTIVE_FRAMES:
self.angle_limit_counter = 0
can_sends.append(hyundaican.create_lkas11(self.packer, self.frame, self.car_fingerprint, apply_steer, lat_active, can_sends.append(hyundaican.create_lkas11(self.packer, self.frame, self.car_fingerprint, apply_steer, lat_active,
torque_fault, CS.lkas11, sys_warning, sys_state, CC.enabled, torque_fault, CS.lkas11, sys_warning, sys_state, CC.enabled,
hud_control.leftLaneVisible, hud_control.rightLaneVisible, hud_control.leftLaneVisible, hud_control.rightLaneVisible,

@ -0,0 +1,29 @@
#!/usr/bin/env python3
import unittest
from cereal import car
from selfdrive.car.car_helpers import get_interface_attr
from selfdrive.car.fw_versions import FW_QUERY_CONFIGS
from selfdrive.car.hyundai.values import CANFD_CAR
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 TestHyundaiFingerprint(unittest.TestCase):
def test_auxiliary_request_ecu_whitelist(self):
# Asserts only auxiliary Ecus can exist in database for CAN-FD cars
config = FW_QUERY_CONFIGS['hyundai']
whitelisted_ecus = {ecu for r in config.requests for ecu in r.whitelist_ecus if r.bus > 3}
for car_model in CANFD_CAR:
ecus = {fw[0] for fw in VERSIONS['hyundai'][car_model].keys()}
ecus_not_in_whitelist = ecus - whitelisted_ecus
ecu_strings = ", ".join([f'Ecu.{ECU_NAME[ecu]}' for ecu in ecus_not_in_whitelist])
self.assertEqual(len(ecus_not_in_whitelist), 0, f'{car_model}: Car model has ECUs not in auxiliary request whitelists: {ecu_strings}')
if __name__ == "__main__":
unittest.main()

@ -300,10 +300,25 @@ FW_QUERY_CONFIG = FwQueryConfig(
Request( Request(
[HYUNDAI_VERSION_REQUEST_LONG], [HYUNDAI_VERSION_REQUEST_LONG],
[HYUNDAI_VERSION_RESPONSE], [HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.transmission, Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera],
), ),
Request( Request(
[HYUNDAI_VERSION_REQUEST_MULTI], [HYUNDAI_VERSION_REQUEST_MULTI],
[HYUNDAI_VERSION_RESPONSE], [HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.engine, Ecu.transmission, Ecu.eps, Ecu.abs, Ecu.fwdRadar],
),
# CAN-FD queries
Request(
[HYUNDAI_VERSION_REQUEST_LONG],
[HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.fwdRadar],
bus=4,
),
Request(
[HYUNDAI_VERSION_REQUEST_LONG],
[HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.fwdCamera, Ecu.adas],
bus=5,
), ),
], ],
extra_ecus=[ extra_ecus=[
@ -748,6 +763,7 @@ FW_VERSIONS = {
b'\xf1\x81640E0051\x00\x00\x00\x00\x00\x00\x00\x00', b'\xf1\x81640E0051\x00\x00\x00\x00\x00\x00\x00\x00',
b'\xf1\x82CKJN3TMSDE0B\x00\x00\x00\x00', b'\xf1\x82CKJN3TMSDE0B\x00\x00\x00\x00',
b'\xf1\x82CKKN3TMD_H0A\x00\x00\x00\x00', b'\xf1\x82CKKN3TMD_H0A\x00\x00\x00\x00',
b'\xe0\x19\xff\xe7\xe7g\x01\xa2\x00\x0f\x00\x9e\x00\x06\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\x0f\x0e\x0f\x0f\x0e\r\x00\x00\x7f\x02.\xff\x00\x00~p\x00\x00\x00\x00u\xff\xf9\xff\x00\x00\x00\x00V\t\xd5\x01\xc0\x00\x00\x00\x007\xfb\xfc\x0b\x8d\x00',
], ],
(Ecu.eps, 0x7d4, None): [ (Ecu.eps, 0x7d4, None): [
b'\xf1\x00CK MDPS R 1.00 1.04 57700-J5200 4C2CL104', b'\xf1\x00CK MDPS R 1.00 1.04 57700-J5200 4C2CL104',
@ -770,6 +786,8 @@ FW_VERSIONS = {
b'\xf1\x87VDKLJ18675252DK6\x89vhgwwwwveVU\x88w\x87w\x99vgf\x97vXfgw_\xff\xc2\xfb\xf1\x89E25\x00\x00\x00\x00\x00\x00\x00\xf1\x82TCK0T33NB2', b'\xf1\x87VDKLJ18675252DK6\x89vhgwwwwveVU\x88w\x87w\x99vgf\x97vXfgw_\xff\xc2\xfb\xf1\x89E25\x00\x00\x00\x00\x00\x00\x00\xf1\x82TCK0T33NB2',
b'\xf1\x87WAJTE17552812CH4vfFffvfVeT5DwvvVVdFeegeg\x88\x88o\xff\x1a]\xf1\x81E21\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 E21\x00\x00\x00\x00\x00\x00\x00TCK2T20NB1\x19\xd2\x00\x94', b'\xf1\x87WAJTE17552812CH4vfFffvfVeT5DwvvVVdFeegeg\x88\x88o\xff\x1a]\xf1\x81E21\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 E21\x00\x00\x00\x00\x00\x00\x00TCK2T20NB1\x19\xd2\x00\x94',
b'\xf1\x87VDHLG17274082DK2wfFf\x89x\x98wUT5T\x88v\x97xgeGefTGTVvO\xff\x1c\x14\xf1\x81E19\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 E19\x00\x00\x00\x00\x00\x00\x00SCK0T33UB2\xee[\x97S', b'\xf1\x87VDHLG17274082DK2wfFf\x89x\x98wUT5T\x88v\x97xgeGefTGTVvO\xff\x1c\x14\xf1\x81E19\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 E19\x00\x00\x00\x00\x00\x00\x00SCK0T33UB2\xee[\x97S',
b'\xf1\x87VDHLG17000192DK2xdFffT\xa5VUD$DwT\x86wveVeeD&T\x99\xba\x8f\xff\xcc\x99\xf1\x81E21\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 E21\x00\x00\x00\x00\x00\x00\x00SCK0T33NB0\t\xb7\x17\xf5',
b'\xf1\x00bcsh8p54 E21\x00\x00\x00\x00\x00\x00\x00SCK0T33NB0\t\xb7\x17\xf5',
], ],
}, },
CAR.PALISADE: { CAR.PALISADE: {
@ -1328,15 +1346,6 @@ FW_VERSIONS = {
], ],
}, },
CAR.KIA_EV6: { CAR.KIA_EV6: {
(Ecu.abs, 0x7d1, None): [
b'\xf1\x00CV IEB \x02 101!\x10\x18 58520-CV100',
b'\xf1\x00CV IEB \x03 101!\x10\x18 58520-CV100',
b'\xf1\x8758520CV100\xf1\x00CV IEB \x02 101!\x10\x18 58520-CV100',
],
(Ecu.eps, 0x7d4, None): [
b'\xf1\x00CV1 MDPS R 1.00 1.04 57700-CV000 1B30',
b'\xf1\x00CV1 MDPS R 1.00 1.05 57700-CV000 2425',
],
(Ecu.fwdRadar, 0x7d0, None): [ (Ecu.fwdRadar, 0x7d0, None): [
b'\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 ', b'\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 ',
b'\xf1\x8799110CV000\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 ', b'\xf1\x8799110CV000\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 ',
@ -1348,16 +1357,6 @@ FW_VERSIONS = {
], ],
}, },
CAR.IONIQ_5: { CAR.IONIQ_5: {
(Ecu.abs, 0x7d1, None): [
b'\xf1\x00NE1 IEB \x07 106!\x11) 58520-GI010',
b'\xf1\x8758520GI010\xf1\x00NE1 IEB \x07 106!\x11) 58520-GI010',
b'\xf1\x00NE1 IEB \x08 104!\x04\x05 58520-GI000',
b'\xf1\x8758520GI000\xf1\x00NE1 IEB \x08 104!\x04\x05 58520-GI000',
],
(Ecu.eps, 0x7d4, None): [
b'\xf1\x00NE MDPS R 1.00 1.06 57700GI000 4NEDR106',
b'\xf1\x8757700GI000 \xf1\x00NE MDPS R 1.00 1.06 57700GI000 4NEDR106',
],
(Ecu.fwdRadar, 0x7d0, None): [ (Ecu.fwdRadar, 0x7d0, None): [
b'\xf1\x00NE1_ RDR ----- 1.00 1.00 99110-GI000 ', b'\xf1\x00NE1_ RDR ----- 1.00 1.00 99110-GI000 ',
b'\xf1\x8799110GI000\xf1\x00NE1_ RDR ----- 1.00 1.00 99110-GI000 ', b'\xf1\x8799110GI000\xf1\x00NE1_ RDR ----- 1.00 1.00 99110-GI000 ',
@ -1371,15 +1370,8 @@ FW_VERSIONS = {
(Ecu.fwdCamera, 0x7c4, None): [ (Ecu.fwdCamera, 0x7c4, None): [
b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9240 14Q', b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9240 14Q',
], ],
(Ecu.eps, 0x7d4, None): [ (Ecu.fwdRadar, 0x7d0, None): [
b'\xf1\x00NX4 MDPS C 1.00 1.01 56300-P0100 2228', b'\xf1\x00NX4__ 1.00 1.00 99110-N9100 ',
],
(Ecu.engine, 0x7e0, None): [
b'\xf1\x87391312MND0',
],
(Ecu.transmission, 0x7e1, None): [
b'\xf1\x00PSBG2441 G19_Rev\x00\x00\x00SNX4T16XXHS01NS2lS\xdfa',
b'\xf1\x8795441-3D220\x00\xf1\x81G19_Rev\x00\x00\x00\xf1\x00PSBG2441 G19_Rev\x00\x00\x00SNX4T16XXHS01NS2lS\xdfa',
], ],
}, },
CAR.SANTA_CRUZ_1ST_GEN: { CAR.SANTA_CRUZ_1ST_GEN: {

@ -82,30 +82,30 @@ class Controls:
self.log_sock = messaging.sub_sock('androidLog') self.log_sock = messaging.sub_sock('androidLog')
if CI is None:
# wait for one pandaState and one CAN packet
print("Waiting for CAN messages...")
get_one_can(self.can_sock)
self.CI, self.CP = get_car(self.can_sock, self.pm.sock['sendcan'])
else:
self.CI, self.CP = CI, CI.CP
params = Params() params = Params()
self.joystick_mode = params.get_bool("JoystickDebugMode") or (self.CP.notCar and sm is None)
joystick_packet = ['testJoystick'] if self.joystick_mode else []
self.sm = sm self.sm = sm
if self.sm is None: if self.sm is None:
ignore = [] ignore = ['testJoystick']
if SIMULATION: if SIMULATION:
ignore += ['driverCameraState', 'managerState'] ignore += ['driverCameraState', 'managerState']
if params.get_bool('WideCameraOnly'): if params.get_bool('WideCameraOnly'):
ignore += ['roadCameraState'] ignore += ['roadCameraState']
self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration',
'driverMonitoringState', 'longitudinalPlan', 'lateralPlan', 'liveLocationKalman', 'driverMonitoringState', 'longitudinalPlan', 'lateralPlan', 'liveLocationKalman',
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters'] + self.camera_packets + joystick_packet, 'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters', 'testJoystick'] + self.camera_packets,
ignore_alive=ignore, ignore_avg_freq=['radarState', 'longitudinalPlan']) ignore_alive=ignore, ignore_avg_freq=['radarState', 'longitudinalPlan', 'testJoystick'])
if CI is None:
# wait for one pandaState and one CAN packet
print("Waiting for CAN messages...")
get_one_can(self.can_sock)
num_pandas = len(messaging.recv_one_retry(self.sm.sock['pandaStates']).pandaStates)
self.CI, self.CP = get_car(self.can_sock, self.pm.sock['sendcan'], num_pandas)
else:
self.CI, self.CP = CI, CI.CP
self.joystick_mode = params.get_bool("JoystickDebugMode") or (self.CP.notCar and sm is None)
# set alternative experiences from parameters # set alternative experiences from parameters
self.disengage_on_accelerator = params.get_bool("DisengageOnAccelerator") self.disengage_on_accelerator = params.get_bool("DisengageOnAccelerator")

@ -94,6 +94,9 @@ class TestStartup(unittest.TestCase):
time.sleep(2) # wait for controlsd to be ready time.sleep(2) # wait for controlsd to be ready
pm.send('can', can_list_to_can_capnp([[0, 0, b"", 0]]))
time.sleep(0.1)
msg = messaging.new_message('pandaStates', 1) msg = messaging.new_message('pandaStates', 1)
msg.pandaStates[0].pandaType = log.PandaState.PandaType.uno msg.pandaStates[0].pandaType = log.PandaState.PandaType.uno
pm.send('pandaStates', msg) pm.send('pandaStates', msg)

@ -19,7 +19,7 @@ if __name__ == "__main__":
for m in msgs: for m in msgs:
ts[s].append(m.logMonoTime / 1e6) ts[s].append(m.logMonoTime / 1e6)
if len(ts[s]): if len(ts[s]) > 2:
d = np.diff(ts[s]) d = np.diff(ts[s])
print(f"{s:25} {np.mean(d):.2f} {np.std(d):.2f} {np.max(d):.2f} {np.min(d):.2f}") print(f"{s:25} {np.mean(d):.2f} {np.std(d):.2f} {np.max(d):.2f} {np.min(d):.2f}")
time.sleep(1) time.sleep(1)

@ -55,7 +55,7 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct
int bytes_count = 0; int bytes_count = 0;
// extract the message // extract the message
capnp::FlatArrayMessageReader cmsg(kj::ArrayPtr<capnp::word>((capnp::word *)msg->getData(), msg->getSize())); capnp::FlatArrayMessageReader cmsg(kj::ArrayPtr<capnp::word>((capnp::word *)msg->getData(), msg->getSize() / sizeof(capnp::word)));
auto event = cmsg.getRoot<cereal::Event>(); auto event = cmsg.getRoot<cereal::Event>();
auto edata = (name == "driverEncodeData") ? event.getDriverEncodeData() : auto edata = (name == "driverEncodeData") ? event.getDriverEncodeData() :
((name == "wideRoadEncodeData") ? event.getWideRoadEncodeData() : ((name == "wideRoadEncodeData") ? event.getWideRoadEncodeData() :

@ -1,2 +1,2 @@
ee8f830b-d6a1-42ef-9b1b-50fd0b2faae4 d1124586-761e-4e18-a771-6b5ef35124fe
cac8f7b69d420506707ff7a19d573d5011ef2533 6fec774f513a19e44d4316e46ad38277197d45ea

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:932e589e5cce66e5d9f48492426a33c74cd7f352a870d3ddafcede3e9156f30d oid sha256:517262fa9f1ad3cc8049ad3722903f40356d87ea423ee5cf011226fb6cfc3d5b
size 9157561 size 16072278

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3587976a8b7e3be274fa86c2e2233e3e464cad713f5077c4394cd1ddd3c7c6c5 oid sha256:64b94659226a1e3c6594a13c2e5d029465d5803a5c3005121ec7217acdbbef20
size 2636965 size 4443461

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c4d37af666344af6bb218e0b939b1152ad3784c15ac79e37bcf0124643c8286a oid sha256:022a830c39267f378f45204682060c93e3aa304bbd8cfa6b2dfe4fa8f419102d
size 58539563 size 56972617

@ -102,6 +102,7 @@ void get_testframe(int index, std::unique_ptr<zdl::DlSystem::ITensor> &input) {
fread(frame_buffer, length, 1, pFile); fread(frame_buffer, length, 1, pFile);
// std::cout << *(frame_buffer+length/4-1) << std::endl; // std::cout << *(frame_buffer+length/4-1) << std::endl;
std::copy(frame_buffer, frame_buffer+(length/4), input->begin()); std::copy(frame_buffer, frame_buffer+(length/4), input->begin());
fclose(pFile);
} }
void SaveITensor(const std::string& path, const zdl::DlSystem::ITensor* tensor) void SaveITensor(const std::string& path, const zdl::DlSystem::ITensor* tensor)

@ -29,10 +29,10 @@ class DRIVER_MONITOR_SETTINGS():
self._FACE_THRESHOLD = 0.7 self._FACE_THRESHOLD = 0.7
self._EYE_THRESHOLD = 0.65 self._EYE_THRESHOLD = 0.65
self._SG_THRESHOLD = 0.9 self._SG_THRESHOLD = 0.9
self._BLINK_THRESHOLD = 0.87 self._BLINK_THRESHOLD = 0.895
self._EE_THRESH11 = 0.75 self._EE_THRESH11 = 0.275
self._EE_THRESH12 = 3.25 self._EE_THRESH12 = 3.0
self._EE_THRESH21 = 0.01 self._EE_THRESH21 = 0.01
self._EE_THRESH22 = 0.35 self._EE_THRESH22 = 0.35
@ -207,11 +207,11 @@ class DriverStatus():
ee1_dist = self.eev1 > self.ee1_offseter.filtered_stat.M * self.settings._EE_THRESH12 ee1_dist = self.eev1 > self.ee1_offseter.filtered_stat.M * self.settings._EE_THRESH12
else: else:
ee1_dist = self.eev1 > self.settings._EE_THRESH11 ee1_dist = self.eev1 > self.settings._EE_THRESH11
if self.ee2_calibrated: # if self.ee2_calibrated:
ee2_dist = self.eev2 < self.ee2_offseter.filtered_stat.M * self.settings._EE_THRESH22 # ee2_dist = self.eev2 < self.ee2_offseter.filtered_stat.M * self.settings._EE_THRESH22
else: # else:
ee2_dist = self.eev2 < self.settings._EE_THRESH21 # ee2_dist = self.eev2 < self.settings._EE_THRESH21
if ee1_dist or ee2_dist: if ee1_dist:
distracted_types.append(DistractedType.DISTRACTED_E2E) distracted_types.append(DistractedType.DISTRACTED_E2E)
return distracted_types return distracted_types
@ -257,12 +257,11 @@ class DriverStatus():
self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD
self.blink.left_blink = driver_data.leftBlinkProb * (driver_data.leftEyeProb > self.settings._EYE_THRESHOLD) * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) self.blink.left_blink = driver_data.leftBlinkProb * (driver_data.leftEyeProb > self.settings._EYE_THRESHOLD) * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.blink.right_blink = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) self.blink.right_blink = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.eev1 = driver_data.notReadyProb[1] self.eev1 = driver_data.notReadyProb[0]
self.eev2 = driver_data.readyProb[0] self.eev2 = driver_data.readyProb[0]
self.distracted_types = self._get_distracted_types() self.distracted_types = self._get_distracted_types()
self.driver_distracted = (DistractedType.DISTRACTED_POSE in self.distracted_types or self.driver_distracted = (DistractedType.DISTRACTED_E2E in self.distracted_types or DistractedType.DISTRACTED_POSE in self.distracted_types or DistractedType.DISTRACTED_BLINK in self.distracted_types) and \
DistractedType.DISTRACTED_BLINK in self.distracted_types) and \
driver_data.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std driver_data.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std
self.driver_distraction_filter.update(self.driver_distracted) self.driver_distraction_filter.update(self.driver_distracted)

@ -1 +1 @@
c171250d2cc013b3eca1cda4fb62f3d0dda28d4d 865885fc49b2766326568e5cc7ec06be8a3f6fad

@ -1 +1 @@
e5a86c14e2318f2dd218b3985cdbea6f875f7d83 6bb7d8baae51d88dd61f0baf561e386664ddd266

@ -120,8 +120,8 @@ class TestOnroad(unittest.TestCase):
if "DEBUG" in os.environ: if "DEBUG" in os.environ:
segs = filter(lambda x: os.path.exists(os.path.join(x, "rlog")), Path(ROOT).iterdir()) segs = filter(lambda x: os.path.exists(os.path.join(x, "rlog")), Path(ROOT).iterdir())
segs = sorted(segs, key=lambda x: x.stat().st_mtime) segs = sorted(segs, key=lambda x: x.stat().st_mtime)
print(segs[-1]) print(segs[-2])
cls.lr = list(LogReader(os.path.join(segs[-1], "rlog"))) cls.lr = list(LogReader(os.path.join(segs[-2], "rlog")))
return return
# setup env # setup env
@ -187,6 +187,25 @@ class TestOnroad(unittest.TestCase):
big_logs = [f for f, n in cnt.most_common(3) if n / sum(cnt.values()) > 30.] big_logs = [f for f, n in cnt.most_common(3) if n / sum(cnt.values()) > 30.]
self.assertEqual(len(big_logs), 0, f"Log spam: {big_logs}") self.assertEqual(len(big_logs), 0, f"Log spam: {big_logs}")
def test_ui_timings(self):
result = "\n"
result += "------------------------------------------------\n"
result += "-------------- UI Draw Timing ------------------\n"
result += "------------------------------------------------\n"
ts = [m.uiDebug.drawTimeMillis for m in self.lr if m.which() == 'uiDebug']
result += f"min {min(ts):.2f}ms\n"
result += f"max {max(ts):.2f}ms\n"
result += f"std {np.std(ts):.2f}ms\n"
result += f"mean {np.mean(ts):.2f}ms\n"
result += "------------------------------------------------\n"
print(result)
self.assertGreater(len(ts), 20*50, "insufficient samples")
self.assertLess(max(ts), 30.)
self.assertLess(np.mean(ts), 10.)
self.assertLess(np.std(ts), 5.)
def test_cpu_usage(self): def test_cpu_usage(self):
proclogs = [m for m in self.lr if m.which() == 'procLog'] proclogs = [m for m in self.lr if m.which() == 'procLog']
self.assertGreater(len(proclogs), service_list['procLog'].frequency * 45, "insufficient samples") self.assertGreater(len(proclogs), service_list['procLog'].frequency * 45, "insufficient samples")

@ -12,11 +12,11 @@ DriverViewWindow::DriverViewWindow(QWidget* parent) : QWidget(parent) {
layout = new QStackedLayout(this); layout = new QStackedLayout(this);
layout->setStackingMode(QStackedLayout::StackAll); layout->setStackingMode(QStackedLayout::StackAll);
cameraView = new CameraViewWidget("camerad", VISION_STREAM_DRIVER, true, this); cameraView = new CameraWidget("camerad", VISION_STREAM_DRIVER, true, this);
layout->addWidget(cameraView); layout->addWidget(cameraView);
scene = new DriverViewScene(this); scene = new DriverViewScene(this);
connect(cameraView, &CameraViewWidget::vipcThreadFrameReceived, scene, &DriverViewScene::frameUpdated); connect(cameraView, &CameraWidget::vipcThreadFrameReceived, scene, &DriverViewScene::frameUpdated);
layout->addWidget(scene); layout->addWidget(scene);
layout->setCurrentWidget(scene); layout->setCurrentWidget(scene);
} }

@ -42,7 +42,7 @@ protected:
void mouseReleaseEvent(QMouseEvent* e) override; void mouseReleaseEvent(QMouseEvent* e) override;
private: private:
CameraViewWidget *cameraView; CameraWidget *cameraView;
DriverViewScene *scene; DriverViewScene *scene;
QStackedLayout *layout; QStackedLayout *layout;
}; };

@ -368,7 +368,7 @@ void WifiManager::updateGsmSettings(bool roaming, QString apn, bool metered) {
changes = true; changes = true;
} }
int meteredInt = metered ? NM_METERED_NO : NM_METERED_UNKNOWN; int meteredInt = metered ? NM_METERED_UNKNOWN : NM_METERED_NO;
if (settings.value("connection").value("metered").toInt() != meteredInt) { if (settings.value("connection").value("metered").toInt() != meteredInt) {
qWarning() << "Changing connection.metered to" << meteredInt; qWarning() << "Changing connection.metered to" << meteredInt;
settings["connection"]["metered"] = meteredInt; settings["connection"]["metered"] = meteredInt;

@ -18,7 +18,7 @@ OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent) {
stacked_layout->setStackingMode(QStackedLayout::StackAll); stacked_layout->setStackingMode(QStackedLayout::StackAll);
main_layout->addLayout(stacked_layout); main_layout->addLayout(stacked_layout);
nvg = new NvgWindow(VISION_STREAM_ROAD, this); nvg = new AnnotatedCameraWidget(VISION_STREAM_ROAD, this);
QWidget * split_wrapper = new QWidget; QWidget * split_wrapper = new QWidget;
split = new QHBoxLayout(split_wrapper); split = new QHBoxLayout(split_wrapper);
@ -27,7 +27,7 @@ OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent) {
split->addWidget(nvg); split->addWidget(nvg);
if (getenv("DUAL_CAMERA_VIEW")) { if (getenv("DUAL_CAMERA_VIEW")) {
CameraViewWidget *arCam = new CameraViewWidget("camerad", VISION_STREAM_ROAD, true, this); CameraWidget *arCam = new CameraWidget("camerad", VISION_STREAM_ROAD, true, this);
split->insertWidget(0, arCam); split->insertWidget(0, arCam);
} }
@ -173,14 +173,15 @@ void OnroadAlerts::paintEvent(QPaintEvent *event) {
} }
} }
// NvgWindow
NvgWindow::NvgWindow(VisionStreamType type, QWidget* parent) : fps_filter(UI_FREQ, 3, 1. / UI_FREQ), CameraViewWidget("camerad", type, true, parent) { AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget* parent) : fps_filter(UI_FREQ, 3, 1. / UI_FREQ), CameraWidget("camerad", type, true, parent) {
pm = std::make_unique<PubMaster, const std::initializer_list<const char *>>({"uiDebug"});
engage_img = loadPixmap("../assets/img_chffr_wheel.png", {img_size, img_size}); engage_img = loadPixmap("../assets/img_chffr_wheel.png", {img_size, img_size});
dm_img = loadPixmap("../assets/img_driver_face.png", {img_size, img_size}); dm_img = loadPixmap("../assets/img_driver_face.png", {img_size, img_size});
} }
void NvgWindow::updateState(const UIState &s) { void AnnotatedCameraWidget::updateState(const UIState &s) {
const int SET_SPEED_NA = 255; const int SET_SPEED_NA = 255;
const SubMaster &sm = *(s.sm); const SubMaster &sm = *(s.sm);
@ -232,13 +233,13 @@ void NvgWindow::updateState(const UIState &s) {
} }
if (s.scene.calibration_valid) { if (s.scene.calibration_valid) {
CameraViewWidget::updateCalibration(s.scene.view_from_calib); CameraWidget::updateCalibration(s.scene.view_from_calib);
} else { } else {
CameraViewWidget::updateCalibration(DEFAULT_CALIBRATION); CameraWidget::updateCalibration(DEFAULT_CALIBRATION);
} }
} }
void NvgWindow::drawHud(QPainter &p) { void AnnotatedCameraWidget::drawHud(QPainter &p) {
p.save(); p.save();
// Header gradient // Header gradient
@ -400,7 +401,11 @@ void NvgWindow::drawHud(QPainter &p) {
p.restore(); p.restore();
} }
void NvgWindow::drawText(QPainter &p, int x, int y, const QString &text, int alpha) {
// Window that shows camera view and variety of
// info drawn on top
void AnnotatedCameraWidget::drawText(QPainter &p, int x, int y, const QString &text, int alpha) {
QRect real_rect = getTextRect(p, 0, text); QRect real_rect = getTextRect(p, 0, text);
real_rect.moveCenter({x, y - real_rect.height() / 2}); real_rect.moveCenter({x, y - real_rect.height() / 2});
@ -408,7 +413,7 @@ void NvgWindow::drawText(QPainter &p, int x, int y, const QString &text, int alp
p.drawText(real_rect.x(), real_rect.bottom(), text); p.drawText(real_rect.x(), real_rect.bottom(), text);
} }
void NvgWindow::drawIcon(QPainter &p, int x, int y, QPixmap &img, QBrush bg, float opacity) { void AnnotatedCameraWidget::drawIcon(QPainter &p, int x, int y, QPixmap &img, QBrush bg, float opacity) {
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
p.setBrush(bg); p.setBrush(bg);
p.drawEllipse(x - radius / 2, y - radius / 2, radius, radius); p.drawEllipse(x - radius / 2, y - radius / 2, radius, radius);
@ -417,8 +422,8 @@ void NvgWindow::drawIcon(QPainter &p, int x, int y, QPixmap &img, QBrush bg, flo
} }
void NvgWindow::initializeGL() { void AnnotatedCameraWidget::initializeGL() {
CameraViewWidget::initializeGL(); CameraWidget::initializeGL();
qInfo() << "OpenGL version:" << QString((const char*)glGetString(GL_VERSION)); qInfo() << "OpenGL version:" << QString((const char*)glGetString(GL_VERSION));
qInfo() << "OpenGL vendor:" << QString((const char*)glGetString(GL_VENDOR)); qInfo() << "OpenGL vendor:" << QString((const char*)glGetString(GL_VENDOR));
qInfo() << "OpenGL renderer:" << QString((const char*)glGetString(GL_RENDERER)); qInfo() << "OpenGL renderer:" << QString((const char*)glGetString(GL_RENDERER));
@ -428,8 +433,8 @@ void NvgWindow::initializeGL() {
setBackgroundColor(bg_colors[STATUS_DISENGAGED]); setBackgroundColor(bg_colors[STATUS_DISENGAGED]);
} }
void NvgWindow::updateFrameMat() { void AnnotatedCameraWidget::updateFrameMat() {
CameraViewWidget::updateFrameMat(); CameraWidget::updateFrameMat();
UIState *s = uiState(); UIState *s = uiState();
int w = width(), h = height(); int w = width(), h = height();
@ -446,7 +451,7 @@ void NvgWindow::updateFrameMat() {
.translate(-intrinsic_matrix.v[2], -intrinsic_matrix.v[5]); .translate(-intrinsic_matrix.v[2], -intrinsic_matrix.v[5]);
} }
void NvgWindow::drawLaneLines(QPainter &painter, const UIState *s) { void AnnotatedCameraWidget::drawLaneLines(QPainter &painter, const UIState *s) {
painter.save(); painter.save();
const UIScene &scene = s->scene; const UIScene &scene = s->scene;
@ -505,7 +510,7 @@ void NvgWindow::drawLaneLines(QPainter &painter, const UIState *s) {
painter.restore(); painter.restore();
} }
void NvgWindow::drawLead(QPainter &painter, const cereal::ModelDataV2::LeadDataV3::Reader &lead_data, const QPointF &vd) { void AnnotatedCameraWidget::drawLead(QPainter &painter, const cereal::ModelDataV2::LeadDataV3::Reader &lead_data, const QPointF &vd) {
painter.save(); painter.save();
const float speedBuff = 10.; const float speedBuff = 10.;
@ -541,11 +546,13 @@ void NvgWindow::drawLead(QPainter &painter, const cereal::ModelDataV2::LeadDataV
painter.restore(); painter.restore();
} }
void NvgWindow::paintGL() { void AnnotatedCameraWidget::paintGL() {
const double start_draw_t = millis_since_boot();
UIState *s = uiState(); UIState *s = uiState();
const cereal::ModelDataV2::Reader &model = (*s->sm)["modelV2"].getModelV2(); const cereal::ModelDataV2::Reader &model = (*s->sm)["modelV2"].getModelV2();
CameraViewWidget::setFrameId(model.getFrameId()); CameraWidget::setFrameId(model.getFrameId());
CameraViewWidget::paintGL(); CameraWidget::paintGL();
QPainter painter(this); QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::Antialiasing);
@ -575,10 +582,16 @@ void NvgWindow::paintGL() {
LOGW("slow frame rate: %.2f fps", fps); LOGW("slow frame rate: %.2f fps", fps);
} }
prev_draw_t = cur_draw_t; prev_draw_t = cur_draw_t;
// publish debug msg
MessageBuilder msg;
auto m = msg.initEvent().initUiDebug();
m.setDrawTimeMillis(cur_draw_t - start_draw_t);
pm->send("uiDebug", msg);
} }
void NvgWindow::showEvent(QShowEvent *event) { void AnnotatedCameraWidget::showEvent(QShowEvent *event) {
CameraViewWidget::showEvent(event); CameraWidget::showEvent(event);
ui_update_params(uiState()); ui_update_params(uiState());
prev_draw_t = millis_since_boot(); prev_draw_t = millis_since_boot();

@ -25,7 +25,7 @@ private:
}; };
// container window for the NVG UI // container window for the NVG UI
class NvgWindow : public CameraViewWidget { class AnnotatedCameraWidget : public CameraWidget {
Q_OBJECT Q_OBJECT
Q_PROPERTY(float speed MEMBER speed); Q_PROPERTY(float speed MEMBER speed);
Q_PROPERTY(QString speedUnit MEMBER speedUnit); Q_PROPERTY(QString speedUnit MEMBER speedUnit);
@ -43,7 +43,7 @@ class NvgWindow : public CameraViewWidget {
Q_PROPERTY(int status MEMBER status); Q_PROPERTY(int status MEMBER status);
public: public:
explicit NvgWindow(VisionStreamType type, QWidget* parent = 0); explicit AnnotatedCameraWidget(VisionStreamType type, QWidget* parent = 0);
void updateState(const UIState &s); void updateState(const UIState &s);
private: private:
@ -68,6 +68,7 @@ private:
bool has_eu_speed_limit = false; bool has_eu_speed_limit = false;
bool v_ego_cluster_seen = false; bool v_ego_cluster_seen = false;
int status = STATUS_DISENGAGED; int status = STATUS_DISENGAGED;
std::unique_ptr<PubMaster> pm;
protected: protected:
void paintGL() override; void paintGL() override;
@ -97,7 +98,7 @@ private:
void paintEvent(QPaintEvent *event); void paintEvent(QPaintEvent *event);
void mousePressEvent(QMouseEvent* e) override; void mousePressEvent(QMouseEvent* e) override;
OnroadAlerts *alerts; OnroadAlerts *alerts;
NvgWindow *nvg; AnnotatedCameraWidget *nvg;
QColor bg = bg_colors[STATUS_DISENGAGED]; QColor bg = bg_colors[STATUS_DISENGAGED];
QWidget *map = nullptr; QWidget *map = nullptr;
QHBoxLayout* split; QHBoxLayout* split;

@ -93,14 +93,14 @@ mat4 get_fit_view_transform(float widget_aspect_ratio, float frame_aspect_ratio)
} // namespace } // namespace
CameraViewWidget::CameraViewWidget(std::string stream_name, VisionStreamType type, bool zoom, QWidget* parent) : CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, bool zoom, QWidget* parent) :
stream_name(stream_name), stream_type(type), zoomed_view(zoom), QOpenGLWidget(parent) { stream_name(stream_name), stream_type(type), zoomed_view(zoom), QOpenGLWidget(parent) {
setAttribute(Qt::WA_OpaquePaintEvent); setAttribute(Qt::WA_OpaquePaintEvent);
connect(this, &CameraViewWidget::vipcThreadConnected, this, &CameraViewWidget::vipcConnected, Qt::BlockingQueuedConnection); connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection);
connect(this, &CameraViewWidget::vipcThreadFrameReceived, this, &CameraViewWidget::vipcFrameReceived); connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived);
} }
CameraViewWidget::~CameraViewWidget() { CameraWidget::~CameraWidget() {
makeCurrent(); makeCurrent();
if (isValid()) { if (isValid()) {
glDeleteVertexArrays(1, &frame_vao); glDeleteVertexArrays(1, &frame_vao);
@ -111,7 +111,7 @@ CameraViewWidget::~CameraViewWidget() {
doneCurrent(); doneCurrent();
} }
void CameraViewWidget::initializeGL() { void CameraWidget::initializeGL() {
initializeOpenGLFunctions(); initializeOpenGLFunctions();
program = std::make_unique<QOpenGLShaderProgram>(context()); program = std::make_unique<QOpenGLShaderProgram>(context());
@ -161,7 +161,7 @@ void CameraViewWidget::initializeGL() {
#endif #endif
} }
void CameraViewWidget::showEvent(QShowEvent *event) { void CameraWidget::showEvent(QShowEvent *event) {
frames.clear(); frames.clear();
if (!vipc_thread) { if (!vipc_thread) {
vipc_thread = new QThread(); vipc_thread = new QThread();
@ -171,7 +171,7 @@ void CameraViewWidget::showEvent(QShowEvent *event) {
} }
} }
void CameraViewWidget::hideEvent(QHideEvent *event) { void CameraWidget::hideEvent(QHideEvent *event) {
if (vipc_thread) { if (vipc_thread) {
vipc_thread->requestInterruption(); vipc_thread->requestInterruption();
vipc_thread->quit(); vipc_thread->quit();
@ -180,7 +180,7 @@ void CameraViewWidget::hideEvent(QHideEvent *event) {
} }
} }
void CameraViewWidget::updateFrameMat() { void CameraWidget::updateFrameMat() {
int w = width(), h = height(); int w = width(), h = height();
if (zoomed_view) { if (zoomed_view) {
@ -224,12 +224,12 @@ void CameraViewWidget::updateFrameMat() {
} }
} }
void CameraViewWidget::updateCalibration(const mat3 &calib) { void CameraWidget::updateCalibration(const mat3 &calib) {
calibration = calib; calibration = calib;
updateFrameMat(); updateFrameMat();
} }
void CameraViewWidget::paintGL() { void CameraWidget::paintGL() {
glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF()); glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF());
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT); glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
@ -286,7 +286,7 @@ void CameraViewWidget::paintGL() {
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
} }
void CameraViewWidget::vipcConnected(VisionIpcClient *vipc_client) { void CameraWidget::vipcConnected(VisionIpcClient *vipc_client) {
makeCurrent(); makeCurrent();
frames.clear(); frames.clear();
stream_width = vipc_client->buffers[0].width; stream_width = vipc_client->buffers[0].width;
@ -339,7 +339,7 @@ void CameraViewWidget::vipcConnected(VisionIpcClient *vipc_client) {
updateFrameMat(); updateFrameMat();
} }
void CameraViewWidget::vipcFrameReceived(VisionBuf *buf, uint32_t frame_id) { void CameraWidget::vipcFrameReceived(VisionBuf *buf, uint32_t frame_id) {
frames.push_back(std::make_pair(frame_id, buf)); frames.push_back(std::make_pair(frame_id, buf));
while (frames.size() > FRAME_BUFFER_SIZE) { while (frames.size() > FRAME_BUFFER_SIZE) {
frames.pop_front(); frames.pop_front();
@ -347,7 +347,7 @@ void CameraViewWidget::vipcFrameReceived(VisionBuf *buf, uint32_t frame_id) {
update(); update();
} }
void CameraViewWidget::vipcThread() { void CameraWidget::vipcThread() {
VisionStreamType cur_stream_type = stream_type; VisionStreamType cur_stream_type = stream_type;
std::unique_ptr<VisionIpcClient> vipc_client; std::unique_ptr<VisionIpcClient> vipc_client;
VisionIpcBufExtra meta_main = {0}; VisionIpcBufExtra meta_main = {0};

@ -23,13 +23,13 @@
const int FRAME_BUFFER_SIZE = 5; const int FRAME_BUFFER_SIZE = 5;
static_assert(FRAME_BUFFER_SIZE <= YUV_BUFFER_COUNT); static_assert(FRAME_BUFFER_SIZE <= YUV_BUFFER_COUNT);
class CameraViewWidget : public QOpenGLWidget, protected QOpenGLFunctions { class CameraWidget : public QOpenGLWidget, protected QOpenGLFunctions {
Q_OBJECT Q_OBJECT
public: public:
using QOpenGLWidget::QOpenGLWidget; using QOpenGLWidget::QOpenGLWidget;
explicit CameraViewWidget(std::string stream_name, VisionStreamType stream_type, bool zoom, QWidget* parent = nullptr); explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, bool zoom, QWidget* parent = nullptr);
~CameraViewWidget(); ~CameraWidget();
void setStreamType(VisionStreamType type) { stream_type = type; } void setStreamType(VisionStreamType type) { stream_type = type; }
void setBackgroundColor(const QColor &color) { bg = color; } void setBackgroundColor(const QColor &color) { bg = color; }
void setFrameId(int frame_id) { draw_frame_id = frame_id; } void setFrameId(int frame_id) { draw_frame_id = frame_id; }

@ -493,7 +493,7 @@ location set</source>
</message> </message>
</context> </context>
<context> <context>
<name>NvgWindow</name> <name>AnnotatedCameraWidget</name>
<message> <message>
<location filename="../qt/onroad.cc" line="218"/> <location filename="../qt/onroad.cc" line="218"/>
<source>km/h</source> <source>km/h</source>

@ -67,6 +67,29 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context>
<name>AnnotatedCameraWidget</name>
<message>
<source>km/h</source>
<translation>km/h</translation>
</message>
<message>
<source>mph</source>
<translation>mph</translation>
</message>
<message>
<source>MAX</source>
<translation></translation>
</message>
<message>
<source>SPEED</source>
<translation></translation>
</message>
<message>
<source>LIMIT</source>
<translation></translation>
</message>
</context>
<context> <context>
<name>ConfirmationDialog</name> <name>ConfirmationDialog</name>
<message> <message>
@ -406,29 +429,6 @@ location set</source>
<translation></translation> <translation></translation>
</message> </message>
</context> </context>
<context>
<name>NvgWindow</name>
<message>
<source>km/h</source>
<translation>km/h</translation>
</message>
<message>
<source>mph</source>
<translation>mph</translation>
</message>
<message>
<source>MAX</source>
<translation></translation>
</message>
<message>
<source>SPEED</source>
<translation></translation>
</message>
<message>
<source>LIMIT</source>
<translation></translation>
</message>
</context>
<context> <context>
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>

@ -60,11 +60,34 @@
</message> </message>
<message> <message>
<source>Cellular Metered</source> <source>Cellular Metered</source>
<translation type="unfinished"></translation> <translation> </translation>
</message> </message>
<message> <message>
<source>Prevent large data uploads when on a metered connection</source> <source>Prevent large data uploads when on a metered connection</source>
<translation type="unfinished"></translation> <translation> </translation>
</message>
</context>
<context>
<name>AnnotatedCameraWidget</name>
<message>
<source>km/h</source>
<translation>km/h</translation>
</message>
<message>
<source>mph</source>
<translation>mph</translation>
</message>
<message>
<source>MAX</source>
<translation>MAX</translation>
</message>
<message>
<source>SPEED</source>
<translation>SPEED</translation>
</message>
<message>
<source>LIMIT</source>
<translation>LIMIT</translation>
</message> </message>
</context> </context>
<context> <context>
@ -406,29 +429,6 @@ location set</source>
<translation> </translation> <translation> </translation>
</message> </message>
</context> </context>
<context>
<name>NvgWindow</name>
<message>
<source>km/h</source>
<translation>km/h</translation>
</message>
<message>
<source>mph</source>
<translation>mph</translation>
</message>
<message>
<source>MAX</source>
<translation>MAX</translation>
</message>
<message>
<source>SPEED</source>
<translation>SPEED</translation>
</message>
<message>
<source>LIMIT</source>
<translation>LIMIT</translation>
</message>
</context>
<context> <context>
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>

@ -489,7 +489,7 @@ ingesteld</translation>
</message> </message>
</context> </context>
<context> <context>
<name>NvgWindow</name> <name>AnnotatedCameraWidget</name>
<message> <message>
<location filename="../qt/onroad.cc" line="218"/> <location filename="../qt/onroad.cc" line="218"/>
<source>km/h</source> <source>km/h</source>

@ -490,7 +490,7 @@ nie zostało ustawione</translation>
</message> </message>
</context> </context>
<context> <context>
<name>NvgWindow</name> <name>AnnotatedCameraWidget</name>
<message> <message>
<location filename="../qt/onroad.cc" line="218"/> <location filename="../qt/onroad.cc" line="218"/>
<source>km/h</source> <source>km/h</source>

@ -67,6 +67,29 @@
<translation>Evite grandes uploads de dados quando estiver em uma conexão limitada</translation> <translation>Evite grandes uploads de dados quando estiver em uma conexão limitada</translation>
</message> </message>
</context> </context>
<context>
<name>AnnotatedCameraWidget</name>
<message>
<source>km/h</source>
<translation>km/h</translation>
</message>
<message>
<source>mph</source>
<translation>mph</translation>
</message>
<message>
<source>MAX</source>
<translation>LIMITE</translation>
</message>
<message>
<source>SPEED</source>
<translation>MAX</translation>
</message>
<message>
<source>LIMIT</source>
<translation>VELO</translation>
</message>
</context>
<context> <context>
<name>ConfirmationDialog</name> <name>ConfirmationDialog</name>
<message> <message>
@ -407,29 +430,6 @@ trabalho definido</translation>
<translation>Senha incorreta</translation> <translation>Senha incorreta</translation>
</message> </message>
</context> </context>
<context>
<name>NvgWindow</name>
<message>
<source>km/h</source>
<translation>km/h</translation>
</message>
<message>
<source>mph</source>
<translation>mph</translation>
</message>
<message>
<source>MAX</source>
<translation>LIMITE</translation>
</message>
<message>
<source>SPEED</source>
<translation>MAX</translation>
</message>
<message>
<source>LIMIT</source>
<translation>VELO</translation>
</message>
</context>
<context> <context>
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>

@ -488,7 +488,7 @@ location set</source>
</message> </message>
</context> </context>
<context> <context>
<name>NvgWindow</name> <name>AnnotatedCameraWidget</name>
<message> <message>
<location filename="../qt/onroad.cc" line="218"/> <location filename="../qt/onroad.cc" line="218"/>
<source>km/h</source> <source>km/h</source>

@ -67,6 +67,29 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context>
<name>AnnotatedCameraWidget</name>
<message>
<source>km/h</source>
<translation>km/h</translation>
</message>
<message>
<source>mph</source>
<translation>mph</translation>
</message>
<message>
<source>MAX</source>
<translation></translation>
</message>
<message>
<source>SPEED</source>
<translation>SPEED</translation>
</message>
<message>
<source>LIMIT</source>
<translation>LIMIT</translation>
</message>
</context>
<context> <context>
<name>ConfirmationDialog</name> <name>ConfirmationDialog</name>
<message> <message>
@ -404,29 +427,6 @@ location set</source>
<translation></translation> <translation></translation>
</message> </message>
</context> </context>
<context>
<name>NvgWindow</name>
<message>
<source>km/h</source>
<translation>km/h</translation>
</message>
<message>
<source>mph</source>
<translation>mph</translation>
</message>
<message>
<source>MAX</source>
<translation></translation>
</message>
<message>
<source>SPEED</source>
<translation>SPEED</translation>
</message>
<message>
<source>LIMIT</source>
<translation>LIMIT</translation>
</message>
</context>
<context> <context>
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>

@ -67,6 +67,29 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context>
<name>AnnotatedCameraWidget</name>
<message>
<source>km/h</source>
<translation>km/h</translation>
</message>
<message>
<source>mph</source>
<translation>mph</translation>
</message>
<message>
<source>MAX</source>
<translation></translation>
</message>
<message>
<source>SPEED</source>
<translation></translation>
</message>
<message>
<source>LIMIT</source>
<translation></translation>
</message>
</context>
<context> <context>
<name>ConfirmationDialog</name> <name>ConfirmationDialog</name>
<message> <message>
@ -406,29 +429,6 @@ location set</source>
<translation></translation> <translation></translation>
</message> </message>
</context> </context>
<context>
<name>NvgWindow</name>
<message>
<source>km/h</source>
<translation>km/h</translation>
</message>
<message>
<source>mph</source>
<translation>mph</translation>
</message>
<message>
<source>MAX</source>
<translation></translation>
</message>
<message>
<source>SPEED</source>
<translation></translation>
</message>
<message>
<source>LIMIT</source>
<translation></translation>
</message>
</context>
<context> <context>
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>

@ -19,15 +19,15 @@ int main(int argc, char *argv[]) {
{ {
QHBoxLayout *hlayout = new QHBoxLayout(); QHBoxLayout *hlayout = new QHBoxLayout();
layout->addLayout(hlayout); layout->addLayout(hlayout);
hlayout->addWidget(new CameraViewWidget("navd", VISION_STREAM_MAP, false)); hlayout->addWidget(new CameraWidget("navd", VISION_STREAM_MAP, false));
hlayout->addWidget(new CameraViewWidget("camerad", VISION_STREAM_ROAD, false)); hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_ROAD, false));
} }
{ {
QHBoxLayout *hlayout = new QHBoxLayout(); QHBoxLayout *hlayout = new QHBoxLayout();
layout->addLayout(hlayout); layout->addLayout(hlayout);
hlayout->addWidget(new CameraViewWidget("camerad", VISION_STREAM_DRIVER, false)); hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_DRIVER, false));
hlayout->addWidget(new CameraViewWidget("camerad", VISION_STREAM_WIDE_ROAD, false)); hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_WIDE_ROAD, false));
} }
return a.exec(); return a.exec();

@ -365,6 +365,7 @@ class Updater:
["git", "checkout", "--force", "--no-recurse-submodules", "-B", branch, "FETCH_HEAD"], ["git", "checkout", "--force", "--no-recurse-submodules", "-B", branch, "FETCH_HEAD"],
["git", "reset", "--hard"], ["git", "reset", "--hard"],
["git", "clean", "-xdff"], ["git", "clean", "-xdff"],
["git", "submodule", "sync"],
["git", "submodule", "init"], ["git", "submodule", "init"],
["git", "submodule", "update"], ["git", "submodule", "update"],
] ]

@ -21,7 +21,7 @@ class Proc:
PROCS = [ PROCS = [
Proc('camerad', 2.15), Proc('camerad', 2.15),
Proc('modeld', 1.15, atol=0.2), Proc('modeld', 1.15, atol=0.2),
Proc('dmonitoringmodeld', 0.35), Proc('dmonitoringmodeld', 0.4),
Proc('encoderd', 0.23), Proc('encoderd', 0.23),
] ]

@ -12,5 +12,5 @@ else:
qt_libs = ['qt_util', 'Qt5Charts'] + base_libs qt_libs = ['qt_util', 'Qt5Charts'] + base_libs
cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, opendbc,'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv'] + qt_libs cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, opendbc,'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv'] + qt_libs
qt_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc', qt_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc',
'canmessages.cc', 'messageswidget.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) 'canmessages.cc', 'messageswidget.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)

@ -0,0 +1,184 @@
#include "tools/cabana/binaryview.h"
#include <QApplication>
#include <QHeaderView>
#include <QPainter>
#include "tools/cabana/canmessages.h"
// BinaryView
const int CELL_HEIGHT = 30;
BinaryView::BinaryView(QWidget *parent) : QTableView(parent) {
model = new BinaryViewModel(this);
setModel(model);
setItemDelegate(new BinaryItemDelegate(this));
horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
horizontalHeader()->hide();
verticalHeader()->setSectionResizeMode(QHeaderView::Stretch);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// replace selection model
auto old_model = selectionModel();
setSelectionModel(new BinarySelectionModel(model));
delete old_model;
QObject::connect(model, &QAbstractItemModel::modelReset, [this]() {
setFixedHeight((CELL_HEIGHT + 1) * std::min(model->rowCount(), 8) + 2);
});
}
void BinaryView::mouseReleaseEvent(QMouseEvent *event) {
QTableView::mouseReleaseEvent(event);
if (auto indexes = selectedIndexes(); !indexes.isEmpty()) {
int start_bit = indexes.first().row() * 8 + indexes.first().column();
int size = indexes.back().row() * 8 + indexes.back().column() - start_bit + 1;
emit cellsSelected(start_bit, size);
}
}
void BinaryView::setMessage(const QString &message_id) {
msg_id = message_id;
model->setMessage(message_id);
resizeRowsToContents();
clearSelection();
updateState();
}
void BinaryView::updateState() {
model->updateState();
}
// BinaryViewModel
void BinaryViewModel::setMessage(const QString &message_id) {
msg_id = message_id;
beginResetModel();
items.clear();
row_count = 0;
dbc_msg = dbc()->msg(msg_id);
if (dbc_msg) {
row_count = dbc_msg->size;
items.resize(row_count * column_count);
for (int i = 0; i < dbc_msg->sigs.size(); ++i) {
const auto &sig = dbc_msg->sigs[i];
const int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit);
const int end = start + sig.size - 1;
for (int j = start; j <= end; ++j) {
int idx = column_count * (j / 8) + j % 8;
if (idx >= items.size()) {
qWarning() << "signal " << sig.name.c_str() << "out of bounds.start_bit:" << sig.start_bit << "size:" << sig.size;
break;
}
if (j == start) {
sig.is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true;
} else if (j == end) {
sig.is_little_endian ? items[idx].is_msb = true : items[idx].is_lsb = true;
}
items[idx].bg_color = QColor(getColor(i));
}
}
}
endResetModel();
}
QModelIndex BinaryViewModel::index(int row, int column, const QModelIndex &parent) const {
return createIndex(row, column, (void *)&items[row * column_count + column]);
}
Qt::ItemFlags BinaryViewModel::flags(const QModelIndex &index) const {
return (index.column() == column_count - 1) ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
void BinaryViewModel::updateState() {
auto prev_items = items;
const auto &binary = can->lastMessage(msg_id).dat;
// data size may changed.
if (!dbc_msg && binary.size() != row_count) {
beginResetModel();
row_count = binary.size();
items.clear();
items.resize(row_count * column_count);
endResetModel();
}
char hex[3] = {'\0'};
for (int i = 0; i < std::min(binary.size(), row_count); ++i) {
for (int j = 0; j < column_count - 1; ++j) {
items[i * column_count + j].val = QChar((binary[i] >> (7 - j)) & 1 ? '1' : '0');
}
hex[0] = toHex(binary[i] >> 4);
hex[1] = toHex(binary[i] & 0xf);
items[i * column_count + 8].val = hex;
}
for (int i = 0; i < items.size(); ++i) {
if (i >= prev_items.size() || prev_items[i].val != items[i].val) {
auto idx = index(i / column_count, i % column_count);
emit dataChanged(idx, idx);
}
}
}
QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation == Qt::Vertical) {
switch (role) {
case Qt::DisplayRole: return section + 1;
case Qt::SizeHintRole: return QSize(30, CELL_HEIGHT);
case Qt::TextAlignmentRole: return Qt::AlignCenter;
}
}
return {};
}
// BinarySelectionModel
void BinarySelectionModel::select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) {
QItemSelection new_selection = selection;
if (auto indexes = selection.indexes(); !indexes.isEmpty()) {
auto [begin_idx, end_idx] = (QModelIndex[]){indexes.first(), indexes.back()};
for (int row = begin_idx.row(); row <= end_idx.row(); ++row) {
int left_col = (row == begin_idx.row()) ? begin_idx.column() : 0;
int right_col = (row == end_idx.row()) ? end_idx.column() : 7;
new_selection.merge({model()->index(row, left_col), model()->index(row, right_col)}, command);
}
}
QItemSelectionModel::select(new_selection, command);
}
// BinaryItemDelegate
BinaryItemDelegate::BinaryItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {
// cache fonts and color
small_font.setPointSize(6);
bold_font.setBold(true);
highlight_color = QApplication::style()->standardPalette().color(QPalette::Active, QPalette::Highlight);
}
QSize BinaryItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const {
QSize sz = QStyledItemDelegate::sizeHint(option, index);
return {sz.width(), CELL_HEIGHT};
}
void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
auto item = (const BinaryViewModel::Item *)index.internalPointer();
painter->save();
// TODO: highlight signal cells on mouse over
painter->fillRect(option.rect, option.state & QStyle::State_Selected ? highlight_color : item->bg_color);
if (index.column() == 8) {
painter->setFont(bold_font);
}
painter->drawText(option.rect, Qt::AlignCenter, item->val);
if (item->is_msb || item->is_lsb) {
painter->setFont(small_font);
painter->drawText(option.rect, Qt::AlignHCenter | Qt::AlignBottom, item->is_msb ? "MSB" : "LSB");
}
painter->restore();
}

@ -0,0 +1,72 @@
#pragma once
#include <QStyledItemDelegate>
#include <QTableView>
#include "tools/cabana/dbcmanager.h"
class BinaryItemDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
BinaryItemDelegate(QObject *parent);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
private:
QFont small_font, bold_font;
QColor highlight_color;
};
class BinaryViewModel : public QAbstractTableModel {
Q_OBJECT
public:
BinaryViewModel(QObject *parent) : QAbstractTableModel(parent) {}
void setMessage(const QString &message_id);
void updateState();
Qt::ItemFlags flags(const QModelIndex &index) const;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const { return {}; }
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; }
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; }
struct Item {
QColor bg_color = QColor(Qt::white);
bool is_msb = false;
bool is_lsb = false;
QString val = "0";
};
private:
QString msg_id;
const Msg *dbc_msg;
int row_count = 0;
const int column_count = 9;
std::vector<Item> items;
};
// the default QItemSelectionModel does not support our selection mode.
class BinarySelectionModel : public QItemSelectionModel {
public:
BinarySelectionModel(QAbstractItemModel *model = nullptr) : QItemSelectionModel(model) {}
void select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) override;
};
class BinaryView : public QTableView {
Q_OBJECT
public:
BinaryView(QWidget *parent = nullptr);
void mouseReleaseEvent(QMouseEvent *event) override;
void setMessage(const QString &message_id);
void updateState();
signals:
void cellsSelected(int start_bit, int size);
private:
QString msg_id;
BinaryViewModel *model;
};

@ -14,6 +14,7 @@ int main(int argc, char *argv[]) {
cmd_parser.addHelpOption(); cmd_parser.addHelpOption();
cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai"); cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai");
cmd_parser.addOption({"demo", "use a demo route instead of providing your own"}); cmd_parser.addOption({"demo", "use a demo route instead of providing your own"});
cmd_parser.addOption({"qcam", "load qcamera"});
cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"}); cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"});
cmd_parser.process(app); cmd_parser.process(app);
const QStringList args = cmd_parser.positionalArguments(); const QStringList args = cmd_parser.positionalArguments();
@ -23,7 +24,7 @@ int main(int argc, char *argv[]) {
const QString route = args.empty() ? DEMO_ROUTE : args.first(); const QString route = args.empty() ? DEMO_ROUTE : args.first();
CANMessages p(&app); CANMessages p(&app);
if (!p.loadRoute(route, cmd_parser.value("data_dir"), true)) { if (!p.loadRoute(route, cmd_parser.value("data_dir"), cmd_parser.isSet("qcam"))) {
return 0; return 0;
} }
MainWindow w; MainWindow w;

@ -141,11 +141,14 @@ void Settings::save() {
s.setValue("fps", fps); s.setValue("fps", fps);
s.setValue("log_size", can_msg_log_size); s.setValue("log_size", can_msg_log_size);
s.setValue("cached_segment", cached_segment_limit); s.setValue("cached_segment", cached_segment_limit);
s.setValue("chart_height", chart_height);
emit changed();
} }
void Settings::load() { void Settings::load() {
QSettings s("settings", QSettings::IniFormat); QSettings s("settings", QSettings::IniFormat);
fps = s.value("fps", 10).toInt(); fps = s.value("fps", 10).toInt();
can_msg_log_size = s.value("log_size", 100).toInt(); can_msg_log_size = s.value("log_size", 100).toInt();
cached_segment_limit = s.value("cached_segment", 3.).toInt(); cached_segment_limit = s.value("cached_segment", 3).toInt();
chart_height = s.value("chart_height", 200).toInt();
} }

@ -19,6 +19,7 @@ public:
int fps = 10; int fps = 10;
int can_msg_log_size = 100; int can_msg_log_size = 100;
int cached_segment_limit = 3; int cached_segment_limit = 3;
int chart_height = 200;
signals: signals:
void changed(); void changed();
@ -90,6 +91,7 @@ inline char toHex(uint value) {
} }
inline const QString &getColor(int i) { inline const QString &getColor(int i) {
// TODO: add more colors
static const QString SIGNAL_COLORS[] = {"#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF", "#FF7F50", "#FFBF00"}; static const QString SIGNAL_COLORS[] = {"#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF", "#FF7F50", "#FFBF00"};
return SIGNAL_COLORS[i % std::size(SIGNAL_COLORS)]; return SIGNAL_COLORS[i % std::size(SIGNAL_COLORS)];
} }

@ -1,8 +1,8 @@
#include "tools/cabana/chartswidget.h" #include "tools/cabana/chartswidget.h"
#include <QGraphicsLayout> #include <QGraphicsLayout>
#include <QLabel>
#include <QRubberBand> #include <QRubberBand>
#include <QTimer>
#include <QtCharts/QLineSeries> #include <QtCharts/QLineSeries>
#include <QtCharts/QValueAxis> #include <QtCharts/QValueAxis>
@ -14,6 +14,7 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
// title bar // title bar
title_bar = new QWidget(this); title_bar = new QWidget(this);
title_bar->setVisible(false);
QHBoxLayout *title_layout = new QHBoxLayout(title_bar); QHBoxLayout *title_layout = new QHBoxLayout(title_bar);
title_layout->setContentsMargins(0, 0, 0, 0); title_layout->setContentsMargins(0, 0, 0, 0);
title_label = new QLabel(tr("Charts")); title_label = new QLabel(tr("Charts"));
@ -25,13 +26,11 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
title_layout->addWidget(range_label); title_layout->addWidget(range_label);
reset_zoom_btn = new QPushButton("", this); reset_zoom_btn = new QPushButton("", this);
reset_zoom_btn->setVisible(false);
reset_zoom_btn->setFixedSize(30, 30); reset_zoom_btn->setFixedSize(30, 30);
reset_zoom_btn->setToolTip(tr("Reset zoom (drag on chart to zoom X-Axis)")); reset_zoom_btn->setToolTip(tr("Reset zoom (drag on chart to zoom X-Axis)"));
title_layout->addWidget(reset_zoom_btn); title_layout->addWidget(reset_zoom_btn);
remove_all_btn = new QPushButton("", this); remove_all_btn = new QPushButton("", this);
remove_all_btn->setVisible(false);
remove_all_btn->setToolTip(tr("Remove all charts")); remove_all_btn->setToolTip(tr("Remove all charts"));
remove_all_btn->setFixedSize(30, 30); remove_all_btn->setFixedSize(30, 30);
title_layout->addWidget(remove_all_btn); title_layout->addWidget(remove_all_btn);
@ -56,10 +55,20 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
main_layout->addWidget(charts_scroll); main_layout->addWidget(charts_scroll);
updateTitleBar();
QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartsWidget::removeChart);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll); QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll);
QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartsWidget::removeChart);
QObject::connect(dbc(), &DBCManager::signalUpdated, [this](const Signal *sig) {
if (auto it = charts.find(sig); it != charts.end()) {
it.value()->chart_view->updateSeries();
}
});
QObject::connect(dbc(), &DBCManager::msgUpdated, [this](const QString &id) {
for (auto chart : charts) {
if (chart->id == id)
chart->updateTitle();
}
});
QObject::connect(can, &CANMessages::rangeChanged, [this]() { updateTitleBar(); }); QObject::connect(can, &CANMessages::rangeChanged, [this]() { updateTitleBar(); });
QObject::connect(reset_zoom_btn, &QPushButton::clicked, can, &CANMessages::resetRange); QObject::connect(reset_zoom_btn, &QPushButton::clicked, can, &CANMessages::resetRange);
QObject::connect(remove_all_btn, &QPushButton::clicked, this, &ChartsWidget::removeAll); QObject::connect(remove_all_btn, &QPushButton::clicked, this, &ChartsWidget::removeAll);
@ -68,57 +77,51 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
docking = !docking; docking = !docking;
updateTitleBar(); updateTitleBar();
}); });
QObject::connect(&settings, &Settings::changed, [this]() {
for (auto chart : charts) {
chart->setHeight(settings.chart_height);
}
});
} }
void ChartsWidget::updateTitleBar() { void ChartsWidget::updateTitleBar() {
if (!charts.size()) { title_bar->setVisible(!charts.isEmpty());
title_bar->setVisible(false); if (charts.isEmpty()) return;
return;
}
title_label->setText(tr("Charts (%1)").arg(charts.size()));
// show select range // show select range
range_label->setVisible(can->isZoomed());
reset_zoom_btn->setEnabled(can->isZoomed());
if (can->isZoomed()) { if (can->isZoomed()) {
auto [min, max] = can->range(); auto [min, max] = can->range();
range_label->setText(tr("%1 - %2").arg(min, 0, 'f', 2).arg(max, 0, 'f', 2)); range_label->setText(tr("%1 - %2").arg(min, 0, 'f', 2).arg(max, 0, 'f', 2));
range_label->setVisible(true);
reset_zoom_btn->setEnabled(true);
} else {
reset_zoom_btn->setEnabled(false);
range_label->setVisible(false);
} }
title_label->setText(tr("Charts (%1)").arg(charts.size()));
dock_btn->setText(docking ? "" : ""); dock_btn->setText(docking ? "" : "");
dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts"));
remove_all_btn->setVisible(!charts.empty());
reset_zoom_btn->setVisible(!charts.empty());
title_bar->setVisible(true);
} }
void ChartsWidget::addChart(const QString &id, const QString &sig_name) { void ChartsWidget::addChart(const QString &id, const Signal *sig) {
const QString char_name = id + ":" + sig_name; if (!charts.contains(sig)) {
if (charts.find(char_name) == charts.end()) { auto chart = new ChartWidget(id, sig, this);
auto chart = new ChartWidget(id, sig_name, this); QObject::connect(chart, &ChartWidget::remove, [=]() { removeChart(sig); });
QObject::connect(chart, &ChartWidget::remove, [=]() {
removeChart(id, sig_name);
});
charts_layout->insertWidget(0, chart); charts_layout->insertWidget(0, chart);
charts[char_name] = chart; charts.insert(sig, chart);
} }
updateTitleBar(); updateTitleBar();
} }
void ChartsWidget::removeChart(const QString &id, const QString &sig_name) { void ChartsWidget::removeChart(const Signal *sig) {
if (auto it = charts.find(id + ":" + sig_name); it != charts.end()) { auto it = charts.find(sig);
it->second->deleteLater(); if (it != charts.end()) {
charts.erase(it); it.value()->deleteLater();
charts.remove(sig);
} }
updateTitleBar(); updateTitleBar();
} }
void ChartsWidget::removeAll() { void ChartsWidget::removeAll() {
for (auto [_, chart] : charts) for (auto chart : charts)
chart->deleteLater(); chart->deleteLater();
charts.clear(); charts.clear();
updateTitleBar(); updateTitleBar();
@ -134,19 +137,16 @@ bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) {
// ChartWidget // ChartWidget
ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *parent) : id(id), sig_name(sig_name), QWidget(parent) { ChartWidget::ChartWidget(const QString &id, const Signal *sig, QWidget *parent) : id(id), signal(sig), QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->setSpacing(0);
QWidget *chart_widget = new QWidget(this); main_layout->setContentsMargins(0, 0, 0, 0);
QVBoxLayout *chart_layout = new QVBoxLayout(chart_widget);
chart_layout->setSpacing(0);
chart_layout->setContentsMargins(0, 0, 0, 0);
QWidget *header = new QWidget(this); QWidget *header = new QWidget(this);
header->setStyleSheet("background-color:white"); header->setStyleSheet("background-color:white");
QHBoxLayout *header_layout = new QHBoxLayout(header); QHBoxLayout *header_layout = new QHBoxLayout(header);
header_layout->setContentsMargins(11, 11, 11, 0); header_layout->setContentsMargins(11, 11, 11, 0);
QLabel *title = new QLabel(tr("%1 %2").arg(dbc()->msg(id)->name.c_str()).arg(id)); title = new QLabel(tr("%1 %2").arg(dbc()->msg(id)->name.c_str()).arg(id));
header_layout->addWidget(title); header_layout->addWidget(title);
header_layout->addStretch(); header_layout->addStretch();
@ -155,26 +155,28 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa
remove_btn->setToolTip(tr("Remove chart")); remove_btn->setToolTip(tr("Remove chart"));
QObject::connect(remove_btn, &QPushButton::clicked, this, &ChartWidget::remove); QObject::connect(remove_btn, &QPushButton::clicked, this, &ChartWidget::remove);
header_layout->addWidget(remove_btn); header_layout->addWidget(remove_btn);
chart_layout->addWidget(header); main_layout->addWidget(header);
chart_view = new ChartView(id, sig_name, this); chart_view = new ChartView(id, sig, this);
chart_view->setFixedHeight(300); chart_view->setFixedHeight(settings.chart_height);
chart_layout->addWidget(chart_view); main_layout->addWidget(chart_view);
chart_layout->addStretch(); main_layout->addStretch();
main_layout->addWidget(chart_widget);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
} }
void ChartWidget::updateTitle() {
title->setText(tr("%1 %2").arg(dbc()->msg(id)->name.c_str()).arg(id));
}
// ChartView // ChartView
ChartView::ChartView(const QString &id, const QString &sig_name, QWidget *parent) ChartView::ChartView(const QString &id, const Signal *sig, QWidget *parent)
: id(id), sig_name(sig_name), QChartView(nullptr, parent) { : id(id), signal(sig), QChartView(nullptr, parent) {
QLineSeries *series = new QLineSeries(); QLineSeries *series = new QLineSeries();
series->setUseOpenGL(true); series->setUseOpenGL(true);
QChart *chart = new QChart(); QChart *chart = new QChart();
chart->setTitle(sig_name); chart->setTitle(sig->name.c_str());
chart->addSeries(series); chart->addSeries(series);
chart->createDefaultAxes(); chart->createDefaultAxes();
chart->legend()->hide(); chart->legend()->hide();
@ -201,17 +203,36 @@ ChartView::ChartView(const QString &id, const QString &sig_name, QWidget *parent
rubber->setPalette(pal); rubber->setPalette(pal);
} }
QTimer *timer = new QTimer(this);
timer->setInterval(100);
timer->setSingleShot(true);
timer->callOnTimeout(this, &ChartView::adjustChartMargins);
QObject::connect(can, &CANMessages::updated, this, &ChartView::updateState); QObject::connect(can, &CANMessages::updated, this, &ChartView::updateState);
QObject::connect(can, &CANMessages::rangeChanged, this, &ChartView::rangeChanged); QObject::connect(can, &CANMessages::rangeChanged, this, &ChartView::rangeChanged);
QObject::connect(can, &CANMessages::eventsMerged, this, &ChartView::updateSeries); QObject::connect(can, &CANMessages::eventsMerged, this, &ChartView::updateSeries);
QObject::connect(dynamic_cast<QValueAxis *>(chart->axisX()), &QValueAxis::rangeChanged, can, &CANMessages::setRange); QObject::connect(dynamic_cast<QValueAxis *>(chart->axisX()), &QValueAxis::rangeChanged, can, &CANMessages::setRange);
QObject::connect(dbc(), &DBCManager::signalUpdated, [this](const QString &msg_id, const QString &sig_name) { QObject::connect(chart, &QChart::plotAreaChanged, [=](const QRectF &plotArea) {
if (this->id == msg_id && this->sig_name == sig_name) // use a singleshot timer to avoid recursion call.
updateSeries(); timer->start();
}); });
updateSeries(); updateSeries();
} }
void ChartView::adjustChartMargins() {
// TODO: Remove hardcoded aligned_pos
const int aligned_pos = 60;
if (chart()->plotArea().left() != aligned_pos) {
const float left_margin = chart()->margins().left() + aligned_pos - chart()->plotArea().left();
chart()->setMargins(QMargins(left_margin, 0, 0, 0));
}
}
void ChartWidget::setHeight(int height) {
chart_view->setFixedHeight(height);
}
void ChartView::updateState() { void ChartView::updateState() {
auto axis_x = dynamic_cast<QValueAxis *>(chart()->axisX()); auto axis_x = dynamic_cast<QValueAxis *>(chart()->axisX());
int x = chart()->plotArea().left() + chart()->plotArea().width() * (can->currentSec() - axis_x->min()) / (axis_x->max() - axis_x->min()); int x = chart()->plotArea().left() + chart()->plotArea().width() * (can->currentSec() - axis_x->min()) / (axis_x->max() - axis_x->min());
@ -219,9 +240,9 @@ void ChartView::updateState() {
} }
void ChartView::updateSeries() { void ChartView::updateSeries() {
const Signal *sig = dbc()->signal(id, sig_name); chart()->setTitle(signal->name.c_str());
auto events = can->events(); auto events = can->events();
if (!sig || !events) return; if (!events) return;
auto l = id.split(':'); auto l = id.split(':');
int bus = l[0].toInt(); int bus = l[0].toInt();
@ -235,7 +256,7 @@ void ChartView::updateSeries() {
for (auto c : evt->event.getCan()) { for (auto c : evt->event.getCan()) {
if (bus == c.getSrc() && address == c.getAddress()) { if (bus == c.getSrc() && address == c.getAddress()) {
auto dat = c.getDat(); auto dat = c.getDat();
double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *sig); double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *signal);
double ts = (evt->mono_time / (double)1e9) - route_start_time; // seconds double ts = (evt->mono_time / (double)1e9) - route_start_time; // seconds
vals.push_back({ts, value}); vals.push_back({ts, value});
} }

@ -9,7 +9,6 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include <QtCharts/QChartView> #include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#include "tools/cabana/canmessages.h" #include "tools/cabana/canmessages.h"
#include "tools/cabana/dbcmanager.h" #include "tools/cabana/dbcmanager.h"
@ -20,15 +19,16 @@ class ChartView : public QChartView {
Q_OBJECT Q_OBJECT
public: public:
ChartView(const QString &id, const QString &sig_name, QWidget *parent = nullptr); ChartView(const QString &id, const Signal *sig, QWidget *parent = nullptr);
void updateSeries();
private: private:
void mouseReleaseEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *ev) override; void mouseMoveEvent(QMouseEvent *ev) override;
void enterEvent(QEvent *event) override; void enterEvent(QEvent *event) override;
void leaveEvent(QEvent *event) override; void leaveEvent(QEvent *event) override;
void adjustChartMargins();
void updateSeries();
void rangeChanged(qreal min, qreal max); void rangeChanged(qreal min, qreal max);
void updateAxisY(); void updateAxisY();
void updateState(); void updateState();
@ -38,22 +38,24 @@ private:
QGraphicsLineItem *line_marker; QGraphicsLineItem *line_marker;
QList<QPointF> vals; QList<QPointF> vals;
QString id; QString id;
QString sig_name; const Signal *signal;
}; };
class ChartWidget : public QWidget { class ChartWidget : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
ChartWidget(const QString &id, const QString &sig_name, QWidget *parent); ChartWidget(const QString &id, const Signal *sig, QWidget *parent);
inline QChart *chart() const { return chart_view->chart(); } void updateTitle();
void setHeight(int height);
signals: signals:
void remove(); void remove();
protected: public:
QString id; QString id;
QString sig_name; const Signal *signal;
QLabel *title;
ChartView *chart_view = nullptr; ChartView *chart_view = nullptr;
}; };
@ -62,11 +64,8 @@ class ChartsWidget : public QWidget {
public: public:
ChartsWidget(QWidget *parent = nullptr); ChartsWidget(QWidget *parent = nullptr);
void addChart(const QString &id, const QString &sig_name); void addChart(const QString &id, const Signal *sig);
void removeChart(const QString &id, const QString &sig_name); void removeChart(const Signal *sig);
inline bool hasChart(const QString &id, const QString &sig_name) {
return charts.find(id + sig_name) != charts.end();
}
signals: signals:
void dock(bool floating); void dock(bool floating);
@ -85,5 +84,5 @@ private:
QPushButton *reset_zoom_btn; QPushButton *reset_zoom_btn;
QPushButton *remove_all_btn; QPushButton *remove_all_btn;
QVBoxLayout *charts_layout; QVBoxLayout *charts_layout;
std::map<QString, ChartWidget *> charts; QHash<const Signal *, ChartWidget *> charts;
}; };

@ -1,5 +1,6 @@
#include "tools/cabana/dbcmanager.h" #include "tools/cabana/dbcmanager.h"
#include <sstream>
#include <QVector> #include <QVector>
DBCManager::DBCManager(QObject *parent) : QObject(parent) {} DBCManager::DBCManager(QObject *parent) : QObject(parent) {}
@ -16,6 +17,17 @@ void DBCManager::open(const QString &dbc_file_name) {
emit DBCFileChanged(); emit DBCFileChanged();
} }
void DBCManager::open(const QString &name, const QString &content) {
this->dbc_name = name;
std::istringstream stream(content.toStdString());
dbc = const_cast<DBC *>(dbc_parse_from_stream(name.toStdString(), stream));
msg_map.clear();
for (auto &msg : dbc->msgs) {
msg_map[msg.address] = &msg;
}
emit DBCFileChanged();
}
void save(const QString &dbc_file_name) { void save(const QString &dbc_file_name) {
// TODO: save DBC to file // TODO: save DBC to file
} }
@ -36,14 +48,17 @@ void DBCManager::updateMsg(const QString &id, const QString &name, uint32_t size
void DBCManager::addSignal(const QString &id, const Signal &sig) { void DBCManager::addSignal(const QString &id, const Signal &sig) {
if (Msg *m = const_cast<Msg *>(msg(id))) { if (Msg *m = const_cast<Msg *>(msg(id))) {
m->sigs.push_back(sig); m->sigs.push_back(sig);
emit signalAdded(id, QString::fromStdString(sig.name)); emit signalAdded(&m->sigs.back());
} }
} }
void DBCManager::updateSignal(const QString &id, const QString &sig_name, const Signal &sig) { void DBCManager::updateSignal(const QString &id, const QString &sig_name, const Signal &sig) {
if (Signal *s = const_cast<Signal *>(signal(id, sig_name))) { if (Msg *m = const_cast<Msg *>(msg(id))) {
*s = sig; auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); });
emit signalUpdated(id, sig_name); if (it != m->sigs.end()) {
*it = sig;
emit signalUpdated(&(*it));
}
} }
} }
@ -51,21 +66,12 @@ void DBCManager::removeSignal(const QString &id, const QString &sig_name) {
if (Msg *m = const_cast<Msg *>(msg(id))) { if (Msg *m = const_cast<Msg *>(msg(id))) {
auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); });
if (it != m->sigs.end()) { if (it != m->sigs.end()) {
emit signalRemoved(&(*it));
m->sigs.erase(it); m->sigs.erase(it);
emit signalRemoved(id, sig_name);
} }
} }
} }
const Signal *DBCManager::signal(const QString &id, const QString &sig_name) const {
if (auto m = msg(id)) {
auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [&](auto &s) { return sig_name == s.name.c_str(); });
if (it != m->sigs.end())
return &(*it);
}
return nullptr;
}
uint32_t DBCManager::addressFromId(const QString &id) { uint32_t DBCManager::addressFromId(const QString &id) {
return id.mid(id.indexOf(':') + 1).toUInt(nullptr, 16); return id.mid(id.indexOf(':') + 1).toUInt(nullptr, 16);
} }

@ -12,9 +12,9 @@ public:
~DBCManager(); ~DBCManager();
void open(const QString &dbc_file_name); void open(const QString &dbc_file_name);
void open(const QString &name, const QString &content);
void save(const QString &dbc_file_name); void save(const QString &dbc_file_name);
const Signal *signal(const QString &id, const QString &sig_name) const;
void addSignal(const QString &id, const Signal &sig); void addSignal(const QString &id, const Signal &sig);
void updateSignal(const QString &id, const QString &sig_name, const Signal &sig); void updateSignal(const QString &id, const QString &sig_name, const Signal &sig);
void removeSignal(const QString &id, const QString &sig_name); void removeSignal(const QString &id, const QString &sig_name);
@ -31,9 +31,9 @@ public:
} }
signals: signals:
void signalAdded(const QString &id, const QString &sig_name); void signalAdded(const Signal *sig);
void signalRemoved(const QString &id, const QString &sig_name); void signalRemoved(const Signal *sig);
void signalUpdated(const QString &id, const QString &sig_name); void signalUpdated(const Signal *sig);
void msgUpdated(const QString &id); void msgUpdated(const QString &id);
void DBCFileChanged(); void DBCFileChanged();

@ -1,12 +1,12 @@
#include "tools/cabana/detailwidget.h" #include "tools/cabana/detailwidget.h"
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QFormLayout> #include <QFormLayout>
#include <QHeaderView> #include <QMessageBox>
#include <QScrollBar>
#include <QTimer> #include <QTimer>
#include <QVBoxLayout>
#include "tools/cabana/canmessages.h"
#include "tools/cabana/dbcmanager.h"
// DetailWidget // DetailWidget
@ -33,60 +33,52 @@ DetailWidget::DetailWidget(QWidget *parent) : QWidget(parent) {
binary_view = new BinaryView(this); binary_view = new BinaryView(this);
main_layout->addWidget(binary_view, 0, Qt::AlignTop); main_layout->addWidget(binary_view, 0, Qt::AlignTop);
// signal header // signals
signals_header = new QWidget(this); signals_container = new QWidget(this);
QHBoxLayout *signals_header_layout = new QHBoxLayout(signals_header); signals_container->setLayout(new QVBoxLayout);
signals_header_layout->addWidget(new QLabel(tr("Signals"))); signals_container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
signals_header_layout->addStretch();
QPushButton *add_sig_btn = new QPushButton(tr("Add signal"), this);
signals_header_layout->addWidget(add_sig_btn);
signals_header->setVisible(false);
main_layout->addWidget(signals_header);
// scroll area
scroll = new ScrollArea(this); scroll = new ScrollArea(this);
QWidget *container = new QWidget(this); scroll->setWidget(signals_container);
container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
QVBoxLayout *container_layout = new QVBoxLayout(container);
signal_edit_layout = new QVBoxLayout();
signal_edit_layout->setSpacing(2);
container_layout->addLayout(signal_edit_layout);
scroll->setWidget(container);
scroll->setWidgetResizable(true); scroll->setWidgetResizable(true);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
main_layout->addWidget(scroll); main_layout->addWidget(scroll);
// history log
history_log = new HistoryLog(this); history_log = new HistoryLog(this);
main_layout->addWidget(history_log); main_layout->addWidget(history_log);
QObject::connect(add_sig_btn, &QPushButton::clicked, this, &DetailWidget::addSignal);
QObject::connect(edit_btn, &QPushButton::clicked, this, &DetailWidget::editMsg); QObject::connect(edit_btn, &QPushButton::clicked, this, &DetailWidget::editMsg);
QObject::connect(binary_view, &BinaryView::cellsSelected, this, &DetailWidget::addSignal);
QObject::connect(can, &CANMessages::updated, this, &DetailWidget::updateState); QObject::connect(can, &CANMessages::updated, this, &DetailWidget::updateState);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &DetailWidget::dbcMsgChanged);
} }
void DetailWidget::setMessage(const QString &message_id) { void DetailWidget::setMessage(const QString &message_id) {
msg_id = message_id; if (msg_id != message_id) {
for (auto f : signal_forms) { msg_id = message_id;
f->deleteLater(); dbcMsgChanged();
} }
signal_forms.clear(); }
void DetailWidget::dbcMsgChanged() {
if (msg_id.isEmpty()) return;
qDeleteAll(signals_container->findChildren<SignalEdit *>());
QString msg_name = tr("untitled");
if (auto msg = dbc()->msg(msg_id)) { if (auto msg = dbc()->msg(msg_id)) {
for (int i = 0; i < msg->sigs.size(); ++i) { for (int i = 0; i < msg->sigs.size(); ++i) {
auto form = new SignalEdit(i, msg_id, msg->sigs[i], getColor(i)); auto form = new SignalEdit(i, msg_id, msg->sigs[i]);
signal_edit_layout->addWidget(form); signals_container->layout()->addWidget(form);
QObject::connect(form, &SignalEdit::showChart, this, &DetailWidget::showChart); QObject::connect(form, &SignalEdit::showChart, [this, sig = &msg->sigs[i]]() { emit showChart(msg_id, sig); });
QObject::connect(form, &SignalEdit::showFormClicked, this, &DetailWidget::showForm); QObject::connect(form, &SignalEdit::showFormClicked, this, &DetailWidget::showForm);
signal_forms.push_back(form); QObject::connect(form, &SignalEdit::remove, this, &DetailWidget::removeSignal);
QObject::connect(form, &SignalEdit::save, this, &DetailWidget::saveSignal);
} }
name_label->setText(msg->name.c_str()); msg_name = msg->name.c_str();
signals_header->setVisible(true);
} else {
name_label->setText(tr("untitled"));
signals_header->setVisible(false);
} }
edit_btn->setVisible(true); edit_btn->setVisible(true);
name_label->setText(msg_name);
binary_view->setMessage(msg_id); binary_view->setMessage(msg_id);
history_log->setMessage(msg_id); history_log->setMessage(msg_id);
@ -100,141 +92,83 @@ void DetailWidget::updateState() {
history_log->updateState(); history_log->updateState();
} }
void DetailWidget::editMsg() {
EditMessageDialog dlg(msg_id, this);
if (dlg.exec()) {
setMessage(msg_id);
}
}
void DetailWidget::addSignal() {
AddSignalDialog dlg(msg_id, this);
if (dlg.exec()) {
setMessage(msg_id);
}
}
void DetailWidget::showForm() { void DetailWidget::showForm() {
SignalEdit *sender = qobject_cast<SignalEdit *>(QObject::sender()); SignalEdit *sender = qobject_cast<SignalEdit *>(QObject::sender());
if (sender->isFormVisible()) { for (auto f : signals_container->findChildren<SignalEdit *>()) {
sender->setFormVisible(false); f->setFormVisible(f == sender && !f->isFormVisible());
} else { if (f == sender) {
for (auto f : signal_forms) { QTimer::singleShot(0, [=]() { scroll->ensureWidgetVisible(f); });
f->setFormVisible(f == sender);
if (f == sender) {
// scroll to header
QTimer::singleShot(0, [=]() {
const QPoint p = f->mapTo(scroll, QPoint(0, 0));
scroll->verticalScrollBar()->setValue(p.y() + scroll->verticalScrollBar()->value());
});
}
} }
} }
} }
// BinaryView void DetailWidget::editMsg() {
auto msg = dbc()->msg(msg_id);
BinaryView::BinaryView(QWidget *parent) { QString name = msg ? msg->name.c_str() : "untitled";
QVBoxLayout *main_layout = new QVBoxLayout(this); int size = msg ? msg->size : can->lastMessage(msg_id).dat.size();
main_layout->setContentsMargins(0, 0, 0, 0); EditMessageDialog dlg(msg_id, name, size, this);
table = new QTableWidget(this); if (dlg.exec()) {
table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); dbc()->updateMsg(msg_id, dlg.name_edit->text(), dlg.size_spin->value());
table->horizontalHeader()->hide(); dbcMsgChanged();
table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
main_layout->addWidget(table);
table->setColumnCount(9);
}
void BinaryView::setMessage(const QString &message_id) {
msg_id = message_id;
const Msg *msg = dbc()->msg(msg_id);
const int row_count = msg ? msg->size : can->lastMessage(msg_id).dat.size();
table->setRowCount(row_count);
table->setColumnCount(9);
for (int i = 0; i < table->rowCount(); ++i) {
for (int j = 0; j < table->columnCount(); ++j) {
auto item = new QTableWidgetItem();
item->setFlags(item->flags() ^ Qt::ItemIsEditable);
item->setTextAlignment(Qt::AlignCenter);
if (j == 8) {
QFont font;
font.setBold(true);
item->setFont(font);
}
table->setItem(i, j, item);
}
} }
}
// set background color void DetailWidget::addSignal(int start_bit, int size) {
if (msg) { if (dbc()->msg(msg_id)) {
for (int i = 0; i < msg->sigs.size(); ++i) { AddSignalDialog dlg(msg_id, start_bit, size, this);
const auto &sig = msg->sigs[i]; if (dlg.exec()) {
int start = sig.is_little_endian ? sig.start_bit : bigEndianBitIndex(sig.start_bit); dbc()->addSignal(msg_id, dlg.form->getSignal());
for (int j = start; j <= start + sig.size - 1; ++j) { dbcMsgChanged();
table->item(j / 8, j % 8)->setBackground(QColor(getColor(i)));
}
} }
} }
table->setFixedHeight(table->rowHeight(0) * std::min(row_count, 8) + 2);
updateState();
} }
void BinaryView::updateState() { void DetailWidget::saveSignal() {
if (msg_id.isEmpty()) return; SignalEdit *sig_form = qobject_cast<SignalEdit *>(QObject::sender());
auto s = sig_form->form->getSignal();
const auto &binary = can->lastMessage(msg_id).dat; dbc()->updateSignal(msg_id, sig_form->sig_name, s);
// update binary view and history log
binary_view->setMessage(msg_id);
history_log->setMessage(msg_id);
}
setUpdatesEnabled(false); void DetailWidget::removeSignal() {
char hex[3] = {'\0'}; SignalEdit *sig_form = qobject_cast<SignalEdit *>(QObject::sender());
for (int i = 0; i < binary.size(); ++i) { QString text = tr("Are you sure you want to remove signal '%1'").arg(sig_form->sig_name);
for (int j = 0; j < 8; ++j) { if (QMessageBox::Yes == QMessageBox::question(this, tr("Remove signal"), text)) {
table->item(i, j)->setText(QChar((binary[i] >> (7 - j)) & 1 ? '1' : '0')); dbc()->removeSignal(msg_id, sig_form->sig_name);
} dbcMsgChanged();
hex[0] = toHex(binary[i] >> 4);
hex[1] = toHex(binary[i] & 0xf);
table->item(i, 8)->setText(hex);
} }
setUpdatesEnabled(true);
} }
// EditMessageDialog // EditMessageDialog
EditMessageDialog::EditMessageDialog(const QString &msg_id, QWidget *parent) : msg_id(msg_id), QDialog(parent) { EditMessageDialog::EditMessageDialog(const QString &msg_id, const QString &title, int size, QWidget *parent) : QDialog(parent) {
setWindowTitle(tr("Edit message")); setWindowTitle(tr("Edit message"));
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
QFormLayout *form_layout = new QFormLayout(); QFormLayout *form_layout = new QFormLayout();
form_layout->addRow("ID", new QLabel(msg_id)); form_layout->addRow("ID", new QLabel(msg_id));
const auto msg = dbc()->msg(msg_id); name_edit = new QLineEdit(title, this);
name_edit = new QLineEdit(this);
name_edit->setText(msg ? msg->name.c_str() : "untitled");
form_layout->addRow(tr("Name"), name_edit); form_layout->addRow(tr("Name"), name_edit);
size_spin = new QSpinBox(this); size_spin = new QSpinBox(this);
size_spin->setValue(msg ? msg->size : can->lastMessage(msg_id).dat.size()); // TODO: limit the maximum?
size_spin->setMinimum(1);
size_spin->setValue(size);
form_layout->addRow(tr("Size"), size_spin); form_layout->addRow(tr("Size"), size_spin);
main_layout->addLayout(form_layout); main_layout->addLayout(form_layout);
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
main_layout->addWidget(buttonBox); main_layout->addWidget(buttonBox);
setFixedWidth(parent->width() * 0.9); setFixedWidth(parent->width() * 0.9);
connect(buttonBox, &QDialogButtonBox::accepted, this, &EditMessageDialog::save); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
} }
void EditMessageDialog::save() {
const QString name = name_edit->text();
if (size_spin->value() <= 0 || name_edit->text().isEmpty() || name == tr("untitled"))
return;
dbc()->updateMsg(msg_id, name, size_spin->value());
QDialog::accept();
}
// ScrollArea // ScrollArea
bool ScrollArea::eventFilter(QObject *obj, QEvent *ev) { bool ScrollArea::eventFilter(QObject *obj, QEvent *ev) {

@ -1,43 +1,17 @@
#pragma once #pragma once
#include <QDialog>
#include <QLabel>
#include <QPushButton>
#include <QScrollArea> #include <QScrollArea>
#include <QTableWidget>
#include <QVBoxLayout>
#include <QWidget>
#include "opendbc/can/common.h" #include "tools/cabana/binaryview.h"
#include "opendbc/can/common_dbc.h"
#include "tools/cabana/canmessages.h"
#include "tools/cabana/dbcmanager.h"
#include "tools/cabana/historylog.h" #include "tools/cabana/historylog.h"
#include "tools/cabana/signaledit.h" #include "tools/cabana/signaledit.h"
class BinaryView : public QWidget {
Q_OBJECT
public:
BinaryView(QWidget *parent);
void setMessage(const QString &message_id);
void updateState();
private:
QString msg_id;
QTableWidget *table;
};
class EditMessageDialog : public QDialog { class EditMessageDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
EditMessageDialog(const QString &msg_id, QWidget *parent); EditMessageDialog(const QString &msg_id, const QString &title, int size, QWidget *parent);
protected:
void save();
QString msg_id;
QLineEdit *name_edit; QLineEdit *name_edit;
QSpinBox *size_spin; QSpinBox *size_spin;
}; };
@ -57,24 +31,24 @@ class DetailWidget : public QWidget {
public: public:
DetailWidget(QWidget *parent); DetailWidget(QWidget *parent);
void setMessage(const QString &message_id); void setMessage(const QString &message_id);
void dbcMsgChanged();
signals: signals:
void showChart(const QString &msg_id, const QString &sig_name); void showChart(const QString &msg_id, const Signal *sig);
void removeChart(const Signal *sig);
private slots:
void showForm();
private: private:
void addSignal(); void addSignal(int start_bit, int size);
void saveSignal();
void removeSignal();
void editMsg(); void editMsg();
void showForm();
void updateState(); void updateState();
QString msg_id; QString msg_id;
QLabel *name_label, *time_label; QLabel *name_label, *time_label;
QPushButton *edit_btn; QPushButton *edit_btn;
QVBoxLayout *signal_edit_layout; QWidget *signals_container;
QWidget *signals_header;
QList<SignalEdit *> signal_forms;
HistoryLog *history_log; HistoryLog *history_log;
BinaryView *binary_view; BinaryView *binary_view;
ScrollArea *scroll; ScrollArea *scroll;

@ -1,20 +1,20 @@
#include "tools/cabana/historylog.h" #include "tools/cabana/historylog.h"
#include <QFontDatabase>
#include <QHeaderView> #include <QHeaderView>
#include <QVBoxLayout> #include <QVBoxLayout>
QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
bool has_signal = dbc_msg && !dbc_msg->sigs.empty();
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
const auto &can_msgs = can->messages(msg_id); const auto &m = can->messages(msg_id)[index.row()];
if (index.row() < can_msgs.size()) { if (index.column() == 0) {
const auto &can_data = can_msgs[index.row()]; return QString::number(m.ts, 'f', 2);
auto msg = dbc()->msg(msg_id);
if (msg && index.column() < msg->sigs.size()) {
return get_raw_value((uint8_t *)can_data.dat.begin(), can_data.dat.size(), msg->sigs[index.column()]);
} else {
return toHex(can_data.dat);
}
} }
return has_signal ? QString::number(get_raw_value((uint8_t *)m.dat.begin(), m.dat.size(), dbc_msg->sigs[index.column() - 1]))
: toHex(m.dat);
} else if (role == Qt::FontRole && index.column() == 1 && !has_signal) {
return QFontDatabase::systemFont(QFontDatabase::FixedFont);
} }
return {}; return {};
} }
@ -22,8 +22,8 @@ QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
void HistoryLogModel::setMessage(const QString &message_id) { void HistoryLogModel::setMessage(const QString &message_id) {
beginResetModel(); beginResetModel();
msg_id = message_id; msg_id = message_id;
const auto msg = dbc()->msg(message_id); dbc_msg = dbc()->msg(message_id);
column_count = msg && !msg->sigs.empty() ? msg->sigs.size() : 1; column_count = (dbc_msg && !dbc_msg->sigs.empty() ? dbc_msg->sigs.size() : 1) + 1;
row_count = 0; row_count = 0;
endResetModel(); endResetModel();
@ -32,18 +32,14 @@ void HistoryLogModel::setMessage(const QString &message_id) {
QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, int role) const { QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation == Qt::Horizontal) { if (orientation == Qt::Horizontal) {
auto msg = dbc()->msg(msg_id); bool has_signal = dbc_msg && !dbc_msg->sigs.empty();
if (msg && section < msg->sigs.size()) { if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
if (role == Qt::BackgroundRole) { if (section == 0) {
return QBrush(QColor(getColor(section))); return "Time";
} else if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
return QString::fromStdString(msg->sigs[section].name);
} }
} return has_signal ? dbc_msg->sigs[section - 1].name.c_str() : "Data";
} else if (role == Qt::DisplayRole) { } else if (role == Qt::BackgroundRole && section > 0 && has_signal) {
const auto &can_msgs = can->messages(msg_id); return QBrush(QColor(getColor(section - 1)));
if (section < can_msgs.size()) {
return QString::number(can_msgs[section].ts, 'f', 2);
} }
} }
return {}; return {};
@ -52,9 +48,8 @@ QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, i
void HistoryLogModel::updateState() { void HistoryLogModel::updateState() {
if (msg_id.isEmpty()) return; if (msg_id.isEmpty()) return;
const auto &can_msgs = can->messages(msg_id);
int prev_row_count = row_count; int prev_row_count = row_count;
row_count = can_msgs.size(); row_count = can->messages(msg_id).size();
int delta = row_count - prev_row_count; int delta = row_count - prev_row_count;
if (delta > 0) { if (delta > 0) {
beginInsertRows({}, prev_row_count, row_count - 1); beginInsertRows({}, prev_row_count, row_count - 1);
@ -64,8 +59,7 @@ void HistoryLogModel::updateState() {
endRemoveRows(); endRemoveRows();
} }
if (row_count > 0) { if (row_count > 0) {
emit dataChanged(index(0, 0), index(row_count - 1, column_count - 1)); emit dataChanged(index(0, 0), index(row_count - 1, column_count - 1), {Qt::DisplayRole});
emit headerDataChanged(Qt::Vertical, 0, row_count - 1);
} }
} }
@ -75,17 +69,10 @@ HistoryLog::HistoryLog(QWidget *parent) : QWidget(parent) {
model = new HistoryLogModel(this); model = new HistoryLogModel(this);
table = new QTableView(this); table = new QTableView(this);
table->setModel(model); table->setModel(model);
table->horizontalHeader()->setStretchLastSection(true); table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
table->setEditTriggers(QAbstractItemView::NoEditTriggers); table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed);
table->setColumnWidth(0, 60);
table->verticalHeader()->setVisible(false);
table->setStyleSheet("QTableView::item { border:0px; padding-left:5px; padding-right:5px; }"); table->setStyleSheet("QTableView::item { border:0px; padding-left:5px; padding-right:5px; }");
table->verticalHeader()->setStyleSheet("QHeaderView::section {padding-left: 5px; padding-right: 5px;min-width:40px;}");
main_layout->addWidget(table); main_layout->addWidget(table);
} }
void HistoryLog::setMessage(const QString &message_id) {
model->setMessage(message_id);
}
void HistoryLog::updateState() {
model->updateState();
}

@ -6,21 +6,22 @@
#include "tools/cabana/dbcmanager.h" #include "tools/cabana/dbcmanager.h"
class HistoryLogModel : public QAbstractTableModel { class HistoryLogModel : public QAbstractTableModel {
Q_OBJECT Q_OBJECT
public: public:
HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) {} HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) {}
void setMessage(const QString &message_id); void setMessage(const QString &message_id);
void updateState(); void updateState();
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; } int rowCount(const QModelIndex &parent = QModelIndex()) const override { return row_count; }
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return column_count; }
private: private:
QString msg_id; QString msg_id;
int row_count = 0; int row_count = 0;
int column_count = 0; int column_count = 2;
const Msg *dbc_msg = nullptr;
}; };
class HistoryLog : public QWidget { class HistoryLog : public QWidget {
@ -28,8 +29,8 @@ class HistoryLog : public QWidget {
public: public:
HistoryLog(QWidget *parent); HistoryLog(QWidget *parent);
void setMessage(const QString &message_id); void setMessage(const QString &message_id) { model->setMessage(message_id); }
void updateState(); void updateState() { model->updateState(); }
private: private:
QTableView *table; QTableView *table;

@ -5,6 +5,7 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QFormLayout> #include <QFormLayout>
#include <QScreen> #include <QScreen>
#include <QSplitter>
#include <QVBoxLayout> #include <QVBoxLayout>
MainWindow::MainWindow() : QWidget() { MainWindow::MainWindow() : QWidget() {
@ -13,12 +14,16 @@ MainWindow::MainWindow() : QWidget() {
QHBoxLayout *h_layout = new QHBoxLayout(); QHBoxLayout *h_layout = new QHBoxLayout();
main_layout->addLayout(h_layout); main_layout->addLayout(h_layout);
QSplitter *splitter = new QSplitter(Qt::Horizontal, this);
messages_widget = new MessagesWidget(this); messages_widget = new MessagesWidget(this);
h_layout->addWidget(messages_widget); splitter->addWidget(messages_widget);
detail_widget = new DetailWidget(this); detail_widget = new DetailWidget(this);
detail_widget->setFixedWidth(600); splitter->addWidget(detail_widget);
h_layout->addWidget(detail_widget);
splitter->setSizes({100, 500});
h_layout->addWidget(splitter);
// right widgets // right widgets
QWidget *right_container = new QWidget(this); QWidget *right_container = new QWidget(this);
@ -96,6 +101,12 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
cached_segment->setValue(settings.cached_segment_limit); cached_segment->setValue(settings.cached_segment_limit);
form_layout->addRow(tr("Cached segments limit"), cached_segment); form_layout->addRow(tr("Cached segments limit"), cached_segment);
chart_height = new QSpinBox(this);
chart_height->setRange(100, 500);
chart_height->setSingleStep(10);
chart_height->setValue(settings.chart_height);
form_layout->addRow(tr("Chart height"), chart_height);
main_layout->addLayout(form_layout); main_layout->addLayout(form_layout);
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
@ -110,6 +121,7 @@ void SettingsDlg::save() {
settings.fps = fps->value(); settings.fps = fps->value();
settings.can_msg_log_size = log_size->value(); settings.can_msg_log_size = log_size->value();
settings.cached_segment_limit = cached_segment->value(); settings.cached_segment_limit = cached_segment->value();
settings.chart_height = chart_height->value();
settings.save(); settings.save();
accept(); accept();
} }

@ -33,4 +33,5 @@ public:
QSpinBox *fps; QSpinBox *fps;
QSpinBox *log_size ; QSpinBox *log_size ;
QSpinBox *cached_segment; QSpinBox *cached_segment;
QSpinBox *chart_height;
}; };

@ -1,11 +1,13 @@
#include "tools/cabana/messageswidget.h" #include "tools/cabana/messageswidget.h"
#include <QComboBox>
#include <QCompleter> #include <QCompleter>
#include <QDialogButtonBox>
#include <QFontDatabase>
#include <QHeaderView> #include <QHeaderView>
#include <QLineEdit> #include <QLineEdit>
#include <QPushButton> #include <QPushButton>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <QTextEdit>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "tools/cabana/dbcmanager.h" #include "tools/cabana/dbcmanager.h"
@ -15,19 +17,23 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) {
// DBC file selector // DBC file selector
QHBoxLayout *dbc_file_layout = new QHBoxLayout(); QHBoxLayout *dbc_file_layout = new QHBoxLayout();
QComboBox *combo = new QComboBox(this); dbc_combo = new QComboBox(this);
auto dbc_names = dbc()->allDBCNames(); auto dbc_names = dbc()->allDBCNames();
for (const auto &name : dbc_names) { for (const auto &name : dbc_names) {
combo->addItem(QString::fromStdString(name)); dbc_combo->addItem(QString::fromStdString(name));
} }
combo->setEditable(true); dbc_combo->model()->sort(0);
combo->setCurrentText(QString()); dbc_combo->setEditable(true);
combo->setInsertPolicy(QComboBox::NoInsert); dbc_combo->setCurrentText(QString());
combo->completer()->setCompletionMode(QCompleter::PopupCompletion); dbc_combo->setInsertPolicy(QComboBox::NoInsert);
dbc_combo->completer()->setCompletionMode(QCompleter::PopupCompletion);
QFont font; QFont font;
font.setBold(true); font.setBold(true);
combo->lineEdit()->setFont(font); dbc_combo->lineEdit()->setFont(font);
dbc_file_layout->addWidget(combo); dbc_file_layout->addWidget(dbc_combo);
QPushButton *load_from_paste = new QPushButton(tr("Load from paste"), this);
dbc_file_layout->addWidget(load_from_paste);
dbc_file_layout->addStretch(); dbc_file_layout->addStretch();
QPushButton *save_btn = new QPushButton(tr("Save DBC"), this); QPushButton *save_btn = new QPushButton(tr("Save DBC"), this);
@ -62,26 +68,32 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) {
// signals/slots // signals/slots
QObject::connect(filter, &QLineEdit::textChanged, proxy_model, &QSortFilterProxyModel::setFilterFixedString); QObject::connect(filter, &QLineEdit::textChanged, proxy_model, &QSortFilterProxyModel::setFilterFixedString);
QObject::connect(can, &CANMessages::updated, model, &MessageListModel::updateState); QObject::connect(can, &CANMessages::updated, model, &MessageListModel::updateState);
QObject::connect(combo, SIGNAL(activated(const QString &)), SLOT(dbcSelectionChanged(const QString &))); QObject::connect(dbc_combo, SIGNAL(activated(const QString &)), SLOT(dbcSelectionChanged(const QString &)));
QObject::connect(load_from_paste, &QPushButton::clicked, this, &MessagesWidget::loadFromPaste);
QObject::connect(save_btn, &QPushButton::clicked, [=]() { QObject::connect(save_btn, &QPushButton::clicked, [=]() {
// TODO: save DBC to file // TODO: save DBC to file
}); });
QObject::connect(table_widget->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex &current, const QModelIndex &previous) { QObject::connect(table_widget->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex &current, const QModelIndex &previous) {
if (current.isValid()) { if (current.isValid()) {
emit msgSelectionChanged(table_widget->model()->data(current, Qt::UserRole).toString()); emit msgSelectionChanged(current.data(Qt::UserRole).toString());
} }
}); });
// For test purpose // For test purpose
combo->setCurrentText("toyota_nodsu_pt_generated"); dbc_combo->setCurrentText("toyota_nodsu_pt_generated");
} }
void MessagesWidget::dbcSelectionChanged(const QString &dbc_file) { void MessagesWidget::dbcSelectionChanged(const QString &dbc_file) {
dbc()->open(dbc_file); dbc()->open(dbc_file);
// update detailwidget // TODO: reset model?
auto current = table_widget->selectionModel()->currentIndex(); table_widget->sortByColumn(0, Qt::AscendingOrder);
if (current.isValid()) { }
emit msgSelectionChanged(table_widget->model()->data(current, Qt::UserRole).toString());
void MessagesWidget::loadFromPaste() {
LoadDBCDialog dlg(this);
if (dlg.exec()) {
dbc()->open("from paste", dlg.dbc_edit->toPlainText());
dbc_combo->setCurrentText("loaded from paste");
} }
} }
@ -90,9 +102,6 @@ void MessagesWidget::dbcSelectionChanged(const QString &dbc_file) {
QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
return (QString[]){"Name", "ID", "Count", "Bytes"}[section]; return (QString[]){"Name", "ID", "Count", "Bytes"}[section];
else if (orientation == Qt::Vertical && role == Qt::DisplayRole) {
// return QString::number(section);
}
return {}; return {};
} }
@ -100,21 +109,23 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const {
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
auto it = std::next(can->can_msgs.begin(), index.row()); auto it = std::next(can->can_msgs.begin(), index.row());
if (it != can->can_msgs.end() && !it.value().empty()) { if (it != can->can_msgs.end() && !it.value().empty()) {
const auto &d = it.value().front();
const QString &msg_id = it.key(); const QString &msg_id = it.key();
switch (index.column()) { switch (index.column()) {
case 0: { case 0: {
auto msg = dbc()->msg(msg_id); auto msg = dbc()->msg(msg_id);
QString name = msg ? msg->name.c_str() : "untitled"; return msg ? msg->name.c_str() : "untitled";
return name;
} }
case 1: return msg_id; case 1: return msg_id;
case 2: return can->counters[msg_id]; case 2: return can->counters[msg_id];
case 3: return toHex(d.dat); case 3: return toHex(it.value().front().dat);
} }
} }
} else if (role == Qt::UserRole) { } else if (role == Qt::UserRole) {
return std::next(can->can_msgs.begin(), index.row()).key(); return std::next(can->can_msgs.begin(), index.row()).key();
} else if (role == Qt::FontRole) {
if (index.column() == 3) {
return QFontDatabase::systemFont(QFontDatabase::FixedFont);
}
} }
return {}; return {};
} }
@ -132,6 +143,20 @@ void MessageListModel::updateState() {
} }
if (row_count > 0) { if (row_count > 0) {
emit dataChanged(index(0, 0), index(row_count - 1, 3)); emit dataChanged(index(0, 0), index(row_count - 1, 3), {Qt::DisplayRole});
} }
} }
LoadDBCDialog::LoadDBCDialog(QWidget *parent) : QDialog(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this);
dbc_edit = new QTextEdit(this);
dbc_edit->setAcceptRichText(false);
dbc_edit->setPlaceholderText(tr("paste DBC file here"));
main_layout->addWidget(dbc_edit);
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
main_layout->addWidget(buttonBox);
setFixedWidth(640);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}

@ -1,10 +1,21 @@
#pragma once #pragma once
#include <QAbstractTableModel> #include <QAbstractTableModel>
#include <QComboBox>
#include <QDialog>
#include <QTableView> #include <QTableView>
#include <QTextEdit>
#include "tools/cabana/canmessages.h" #include "tools/cabana/canmessages.h"
class LoadDBCDialog : public QDialog {
Q_OBJECT
public:
LoadDBCDialog(QWidget *parent);
QTextEdit *dbc_edit;
};
class MessageListModel : public QAbstractTableModel { class MessageListModel : public QAbstractTableModel {
Q_OBJECT Q_OBJECT
@ -28,11 +39,13 @@ public:
public slots: public slots:
void dbcSelectionChanged(const QString &dbc_file); void dbcSelectionChanged(const QString &dbc_file);
void loadFromPaste();
signals: signals:
void msgSelectionChanged(const QString &message_id); void msgSelectionChanged(const QString &message_id);
protected: protected:
QTableView *table_widget; QTableView *table_widget;
QComboBox *dbc_combo;
MessageListModel *model; MessageListModel *model;
}; };

@ -3,7 +3,6 @@
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QFormLayout> #include <QFormLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMessageBox>
#include <QVBoxLayout> #include <QVBoxLayout>
// SignalForm // SignalForm
@ -15,18 +14,18 @@ SignalForm::SignalForm(const Signal &sig, QWidget *parent) : start_bit(sig.start
form_layout->addRow(tr("Name"), name); form_layout->addRow(tr("Name"), name);
size = new QSpinBox(); size = new QSpinBox();
size->setMinimum(1);
size->setValue(sig.size); size->setValue(sig.size);
form_layout->addRow(tr("Size"), size); form_layout->addRow(tr("Size"), size);
msb = new QSpinBox();
msb->setValue(sig.msb);
form_layout->addRow(tr("Most significant bit"), msb);
endianness = new QComboBox(); endianness = new QComboBox();
endianness->addItems({"Little", "Big"}); endianness->addItems({"Little", "Big"});
endianness->setCurrentIndex(sig.is_little_endian ? 0 : 1); endianness->setCurrentIndex(sig.is_little_endian ? 0 : 1);
form_layout->addRow(tr("Endianness"), endianness); form_layout->addRow(tr("Endianness"), endianness);
form_layout->addRow(tr("lsb"), new QLabel(QString::number(sig.lsb)));
form_layout->addRow(tr("msb"), new QLabel(QString::number(sig.msb)));
sign = new QComboBox(); sign = new QComboBox();
sign->addItems({"Signed", "Unsigned"}); sign->addItems({"Signed", "Unsigned"});
sign->setCurrentIndex(sig.is_signed ? 0 : 1); sign->setCurrentIndex(sig.is_signed ? 0 : 1);
@ -56,7 +55,8 @@ SignalForm::SignalForm(const Signal &sig, QWidget *parent) : start_bit(sig.start
form_layout->addRow(tr("Value descriptions"), val_desc); form_layout->addRow(tr("Value descriptions"), val_desc);
} }
std::optional<Signal> SignalForm::getSignal() { Signal SignalForm::getSignal() {
// TODO: Check if the size is valid, and no duplicate name
Signal sig = {}; Signal sig = {};
sig.start_bit = start_bit; sig.start_bit = start_bit;
sig.name = name->text().toStdString(); sig.name = name->text().toStdString();
@ -72,42 +72,42 @@ std::optional<Signal> SignalForm::getSignal() {
sig.lsb = bigEndianStartBitsIndex(bigEndianBitIndex(sig.start_bit) + sig.size - 1); sig.lsb = bigEndianStartBitsIndex(bigEndianBitIndex(sig.start_bit) + sig.size - 1);
sig.msb = sig.start_bit; sig.msb = sig.start_bit;
} }
return (sig.name.empty() || sig.size <= 0) ? std::nullopt : std::optional(sig); return sig;
} }
// SignalEdit // SignalEdit
SignalEdit::SignalEdit(int index, const QString &id, const Signal &sig, const QString &color, QWidget *parent) SignalEdit::SignalEdit(int index, const QString &msg_id, const Signal &sig, QWidget *parent)
: id(id), name_(sig.name.c_str()), QWidget(parent) { : sig_name(sig.name.c_str()), QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0); main_layout->setContentsMargins(0, 0, 0, 0);
// title // title bar
QHBoxLayout *title_layout = new QHBoxLayout(); QHBoxLayout *title_layout = new QHBoxLayout();
icon = new QLabel(">"); icon = new QLabel(">");
icon->setFixedSize(15, 30);
icon->setStyleSheet("font-weight:bold"); icon->setStyleSheet("font-weight:bold");
title_layout->addWidget(icon); title_layout->addWidget(icon);
title = new ElidedLabel(this); title = new ElidedLabel(this);
title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); title->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
title->setText(QString("%1. %2").arg(index + 1).arg(sig.name.c_str())); title->setText(QString("%1. %2").arg(index + 1).arg(sig_name));
title->setStyleSheet(QString("font-weight:bold; color:%1").arg(color)); title->setStyleSheet(QString("font-weight:bold; color:%1").arg(getColor(index)));
title_layout->addWidget(title); title_layout->addWidget(title, 1);
plot_btn = new QPushButton("📈"); QPushButton *plot_btn = new QPushButton("📈");
plot_btn->setToolTip(tr("Show Plot")); plot_btn->setToolTip(tr("Show Plot"));
plot_btn->setFixedSize(30, 30); plot_btn->setFixedSize(20, 20);
QObject::connect(plot_btn, &QPushButton::clicked, [=]() { emit showChart(id, name_); }); QObject::connect(plot_btn, &QPushButton::clicked, this, &SignalEdit::showChart);
title_layout->addWidget(plot_btn); title_layout->addWidget(plot_btn);
main_layout->addLayout(title_layout); main_layout->addLayout(title_layout);
// signal form
form_container = new QWidget(this); form_container = new QWidget(this);
QVBoxLayout *v_layout = new QVBoxLayout(form_container); QVBoxLayout *v_layout = new QVBoxLayout(form_container);
form = new SignalForm(sig, this); form = new SignalForm(sig, this);
v_layout->addWidget(form); v_layout->addWidget(form);
QHBoxLayout *h = new QHBoxLayout(); QHBoxLayout *h = new QHBoxLayout();
remove_btn = new QPushButton(tr("Remove Signal")); QPushButton *remove_btn = new QPushButton(tr("Remove Signal"));
h->addWidget(remove_btn); h->addWidget(remove_btn);
h->addStretch(); h->addStretch();
QPushButton *save_btn = new QPushButton(tr("Save")); QPushButton *save_btn = new QPushButton(tr("Save"));
@ -117,13 +117,19 @@ SignalEdit::SignalEdit(int index, const QString &id, const Signal &sig, const QS
form_container->setVisible(false); form_container->setVisible(false);
main_layout->addWidget(form_container); main_layout->addWidget(form_container);
QFrame* hline = new QFrame(); // bottom line
QFrame *hline = new QFrame();
hline->setFrameShape(QFrame::HLine); hline->setFrameShape(QFrame::HLine);
hline->setFrameShadow(QFrame::Sunken); hline->setFrameShadow(QFrame::Sunken);
main_layout->addWidget(hline); main_layout->addWidget(hline);
QObject::connect(remove_btn, &QPushButton::clicked, this, &SignalEdit::remove); QObject::connect(remove_btn, &QPushButton::clicked, this, &SignalEdit::remove);
QObject::connect(save_btn, &QPushButton::clicked, this, &SignalEdit::save); QObject::connect(save_btn, &QPushButton::clicked, [=]() {
QString new_name = form->getSignal().name.c_str();
title->setText(QString("%1. %2").arg(index + 1).arg(new_name));
emit save();
sig_name = new_name;
});
QObject::connect(title, &ElidedLabel::clicked, this, &SignalEdit::showFormClicked); QObject::connect(title, &ElidedLabel::clicked, this, &SignalEdit::showFormClicked);
} }
@ -132,40 +138,24 @@ void SignalEdit::setFormVisible(bool visible) {
icon->setText(visible ? "" : ">"); icon->setText(visible ? "" : ">");
} }
void SignalEdit::save() {
if (auto s = form->getSignal())
dbc()->updateSignal(id, name_, *s);
}
void SignalEdit::remove() {
QMessageBox msgbox;
msgbox.setText(tr("Remove signal"));
msgbox.setInformativeText(tr("Are you sure you want to remove signal '%1'").arg(name_));
msgbox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
msgbox.setDefaultButton(QMessageBox::Cancel);
if (msgbox.exec()) {
dbc()->removeSignal(id, name_);
deleteLater();
}
}
// AddSignalDialog // AddSignalDialog
AddSignalDialog::AddSignalDialog(const QString &id, QWidget *parent) : QDialog(parent) { AddSignalDialog::AddSignalDialog(const QString &id, int start_bit, int size, QWidget *parent) : QDialog(parent) {
setWindowTitle(tr("Add signal to %1").arg(dbc()->msg(id)->name.c_str())); setWindowTitle(tr("Add signal to %1").arg(dbc()->msg(id)->name.c_str()));
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
Signal sig = {.name = "untitled"};
auto form = new SignalForm(sig, this); Signal sig = {
.name = "untitled",
.start_bit = bigEndianBitIndex(start_bit),
.is_little_endian = false,
.size = size,
};
form = new SignalForm(sig, this);
main_layout->addWidget(form); main_layout->addWidget(form);
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
main_layout->addWidget(buttonBox); main_layout->addWidget(buttonBox);
setFixedWidth(parent->width() * 0.9); setFixedWidth(parent->width() * 0.9);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(buttonBox, &QDialogButtonBox::accepted, [=]() { connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
if (auto signal = form->getSignal()) {
dbc()->addSignal(id, *signal);
}
QDialog::accept();
});
} }

@ -1,7 +1,5 @@
#pragma once #pragma once
#include <optional>
#include <QComboBox> #include <QComboBox>
#include <QDialog> #include <QDialog>
#include <QLabel> #include <QLabel>
@ -15,14 +13,12 @@
#include "tools/cabana/dbcmanager.h" #include "tools/cabana/dbcmanager.h"
class SignalForm : public QWidget { class SignalForm : public QWidget {
Q_OBJECT
public: public:
SignalForm(const Signal &sig, QWidget *parent); SignalForm(const Signal &sig, QWidget *parent);
std::optional<Signal> getSignal(); Signal getSignal();
QLineEdit *name, *unit, *comment, *val_desc; QLineEdit *name, *unit, *comment, *val_desc;
QSpinBox *size, *msb, *lsb, *offset; QSpinBox *size, *offset;
QDoubleSpinBox *factor, *min_val, *max_val; QDoubleSpinBox *factor, *min_val, *max_val;
QComboBox *sign, *endianness; QComboBox *sign, *endianness;
int start_bit = 0; int start_bit = 0;
@ -32,31 +28,26 @@ class SignalEdit : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
SignalEdit(int index, const QString &id, const Signal &sig, const QString &color, QWidget *parent = nullptr); SignalEdit(int index, const QString &msg_id, const Signal &sig, QWidget *parent = nullptr);
void setFormVisible(bool show); void setFormVisible(bool show);
inline bool isFormVisible() const { return form_container->isVisible(); } inline bool isFormVisible() const { return form_container->isVisible(); }
void save(); QString sig_name;
SignalForm *form;
signals: signals:
void showChart(const QString &msg_id, const QString &sig_name); void showChart();
void showFormClicked(); void showFormClicked();
protected:
void remove(); void remove();
void save();
QString id; protected:
QString name_;
QPushButton *plot_btn;
ElidedLabel *title; ElidedLabel *title;
SignalForm *form;
QWidget *form_container; QWidget *form_container;
QPushButton *remove_btn;
QLabel *icon; QLabel *icon;
}; };
class AddSignalDialog : public QDialog { class AddSignalDialog : public QDialog {
Q_OBJECT
public: public:
AddSignalDialog(const QString &id, QWidget *parent); AddSignalDialog(const QString &id, int start_bit, int size, QWidget *parent);
SignalForm *form;
}; };

@ -5,20 +5,19 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMouseEvent> #include <QMouseEvent>
#include <QPainter> #include <QPainter>
#include <QPushButton>
#include <QStyleOptionSlider> #include <QStyleOptionSlider>
#include <QTimer> #include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
inline QString formatTime(int seconds) { inline QString formatTime(int seconds) {
return QDateTime::fromTime_t(seconds).toString(seconds > 60 * 60 ? "hh::mm::ss" : "mm::ss"); return QDateTime::fromTime_t(seconds).toString(seconds > 60 * 60 ? "hh:mm:ss" : "mm:ss");
} }
VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
// TODO: figure out why the CameraViewWidget crashed occasionally. // TODO: figure out why the CameraWidget crashed occasionally.
cam_widget = new CameraViewWidget("camerad", VISION_STREAM_ROAD, false, this); cam_widget = new CameraWidget("camerad", VISION_STREAM_ROAD, false, this);
cam_widget->setFixedSize(parent->width(), parent->width() / 1.596); cam_widget->setFixedSize(parent->width(), parent->width() / 1.596);
main_layout->addWidget(cam_widget); main_layout->addWidget(cam_widget);
@ -39,9 +38,9 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
// btn controls // btn controls
QHBoxLayout *control_layout = new QHBoxLayout(); QHBoxLayout *control_layout = new QHBoxLayout();
QPushButton *play = new QPushButton(""); play_btn = new QPushButton("");
play->setStyleSheet("font-weight:bold"); play_btn->setStyleSheet("font-weight:bold");
control_layout->addWidget(play); control_layout->addWidget(play_btn);
QButtonGroup *group = new QButtonGroup(this); QButtonGroup *group = new QButtonGroup(this);
group->setExclusive(true); group->setExclusive(true);
@ -61,11 +60,13 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
QObject::connect(can, &CANMessages::updated, this, &VideoWidget::updateState); QObject::connect(can, &CANMessages::updated, this, &VideoWidget::updateState);
QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->value() / 1000.0); }); 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(formatTime(value / 1000)); });
QObject::connect(play, &QPushButton::clicked, [=]() { QObject::connect(cam_widget, &CameraWidget::clicked, [this]() { pause(!can->isPaused()); });
bool is_paused = can->isPaused(); QObject::connect(play_btn, &QPushButton::clicked, [=]() { pause(!can->isPaused()); });
play->setText(is_paused ? "" : ""); }
can->pause(!is_paused);
}); void VideoWidget::pause(bool pause) {
play_btn->setText(!pause ? "" : "");
can->pause(pause);
} }
void VideoWidget::rangeChanged(double min, double max) { void VideoWidget::rangeChanged(double min, double max) {

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <QLabel> #include <QLabel>
#include <QPushButton>
#include <QSlider> #include <QSlider>
#include <QWidget> #include <QWidget>
@ -29,8 +30,10 @@ public:
protected: protected:
void rangeChanged(double min, double max); void rangeChanged(double min, double max);
void updateState(); void updateState();
void pause(bool pause);
CameraViewWidget *cam_widget; CameraWidget *cam_widget;
QLabel *end_time_label; QLabel *end_time_label;
QPushButton *play_btn;
Slider *slider; Slider *slider;
}; };

Loading…
Cancel
Save