mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-15 20:33:41 +08:00
Compare commits
199 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cb30b8142 | |||
| d48cfa730b | |||
| 6e69da4037 | |||
| 6405f15d5a | |||
| ac4ab9a9b8 | |||
| 7461f70fdb | |||
| b7c333cf3f | |||
| fe47e752f6 | |||
| 8cf62e9733 | |||
| d936c7aede | |||
| e7c3736fc7 | |||
| 10502adf95 | |||
| 7a76a50c4f | |||
| 927e822b5a | |||
| f1746f2e2d | |||
| 473ab17876 | |||
| ebc77e4c90 | |||
| ad5afe222d | |||
| 32b518d51d | |||
| 062cb672e8 | |||
| 122a8ca00a | |||
| b1cdf387b5 | |||
| b5b156825d | |||
| eecff73850 | |||
| 6d5d1f6917 | |||
| 434fbad5fc | |||
| 65c007a9a2 | |||
| ea4dbc8936 | |||
| 855c99bf92 | |||
| f8d50e062d | |||
| fedea9fe8d | |||
| 9738db0e39 | |||
| df0bd5e6e3 | |||
| cf790746cc | |||
| 5701fcad05 | |||
| 59f01d212a | |||
| e32b1a344d | |||
| d02355b1a5 | |||
| b755d32276 | |||
| cbda3f7994 | |||
| a75cd2d277 | |||
| 8a88608800 | |||
| 4eb515b32d | |||
| 612d97cfdb | |||
| edd938105e | |||
| 5fadc71a3a | |||
| 0819f5c0fd | |||
| 66b3590b63 | |||
| b77da00697 | |||
| e6939dbede | |||
| 96ca1f8ed5 | |||
| 6f94f217ff | |||
| 03803d0c82 | |||
| be191eb8ac | |||
| 4e9e9190a5 | |||
| ff9c703289 | |||
| 2f4981045b | |||
| faf966adb2 | |||
| bd7b419a4e | |||
| 9bf834e64e | |||
| 4adf52fc6f | |||
| 76e900d3ad | |||
| 155d206b2b | |||
| dd2214a78c | |||
| d252ab5b42 | |||
| 2002a64d3b | |||
| 438035ec17 | |||
| 5406aba1a2 | |||
| 17150b6db0 | |||
| e7b1ee3a5b | |||
| d86686fffd | |||
| a7f32be2fd | |||
| 4db21dd4f8 | |||
| aa0ac919ba | |||
| 2e4de38eb8 | |||
| ed777cf5e2 | |||
| f7bd4002d6 | |||
| 38ef0d2398 | |||
| b361e952c9 | |||
| d4211790da | |||
| b614253eeb | |||
| 6f88b59fdd | |||
| e04313cc91 | |||
| 7ef4304e2e | |||
| afa2b390ae | |||
| 04847f3805 | |||
| 9002016ac5 | |||
| 54a7e8926f | |||
| 20eb0d733a | |||
| f09211fcc5 | |||
| 7dbdff832f | |||
| 88c8f5a520 | |||
| 5037016901 | |||
| bf74ce5447 | |||
| 3643dfbef0 | |||
| cc83d8615d | |||
| d3058de5c1 | |||
| a2ee4dfffc | |||
| b72f6e9f7d | |||
| d4e29ec945 | |||
| e3ba6492f9 | |||
| caee482fe7 | |||
| 1c54ff853a | |||
| c9f0d296b1 | |||
| 6ac27d491b | |||
| 47a7524595 | |||
| 0088cb7ef0 | |||
| 3ec980477d | |||
| e4d152e1a7 | |||
| 4cfdcea1d1 | |||
| 952d2cab8a | |||
| 799551c2d6 | |||
| 0b7bd2561d | |||
| 83000c571c | |||
| 294635beb5 | |||
| 3040f55382 | |||
| b4c9403211 | |||
| e04bb46b68 | |||
| e43de73311 | |||
| 02b6cef15c | |||
| 93f5aa469a | |||
| 74009870ab | |||
| 1d7b6f0f9f | |||
| 98e7c4f987 | |||
| 4dab10e98b | |||
| df90fc2526 | |||
| f29eb5d25c | |||
| 0bf99879d1 | |||
| e1db0f59f1 | |||
| 5bcff3f87d | |||
| 9a39ccc66f | |||
| 27122bbd28 | |||
| c37855d113 | |||
| 9225dac433 | |||
| ee6f374621 | |||
| 0fdfaf737f | |||
| 71e29583cb | |||
| 171f271d11 | |||
| 1a753ad9ae | |||
| 9c312e91e7 | |||
| 4d4d6803e5 | |||
| 9c21b92c4d | |||
| ab7284fc8a | |||
| 554d8d2114 | |||
| b392328c35 | |||
| ddf782f08a | |||
| e04fd3c724 | |||
| 6b47a5b6b7 | |||
| 3601b850c2 | |||
| cfa8e5bf03 | |||
| e1c69719c6 | |||
| a8bb6b5aee | |||
| 87fde8a058 | |||
| 66300306f4 | |||
| 2895346746 | |||
| 49c3b8fc34 | |||
| 0bac3c9039 | |||
| 6335db69bd | |||
| 05f42f752b | |||
| 1791057369 | |||
| 736f3b1a00 | |||
| 2a1c44c904 | |||
| 74ac5ef9a0 | |||
| aac9d9ecf4 | |||
| 08fd571272 | |||
| 75d590fb90 | |||
| 576de9c7ed | |||
| 86753ea93d | |||
| c9b9fd56ea | |||
| 765d6fff9b | |||
| 9e02936717 | |||
| f1977eaa93 | |||
| eb6c9c7f0d | |||
| 1262e26899 | |||
| 911f07ee87 | |||
| ebb5cc12f5 | |||
| 0268dfe79a | |||
| bfe5c079a6 | |||
| f7543e47a0 | |||
| b94f8eaecb | |||
| c41d953be5 | |||
| c603d07012 | |||
| d2ab08eaf2 | |||
| 1086abcf6b | |||
| 617a98d769 | |||
| 7e9de97596 | |||
| 21ab8c9daf | |||
| d671c7e03a | |||
| 7cd9a76a25 | |||
| 7292d409a7 | |||
| 78009bdf39 | |||
| 5e097b2682 | |||
| 31a61a24f8 | |||
| 000c6ce261 | |||
| f6998b3d98 | |||
| c197a1a45a | |||
| 33e3809330 | |||
| df5c0a2690 | |||
| cce352cccb |
+1
-1
@@ -11,4 +11,4 @@
|
||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
|
||||
openpilot/common/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text
|
||||
openpilot/common/hardware/comma/updater filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<!-- Please copy and paste the relevant template -->
|
||||
|
||||
<!--- ***** Template: Fingerprint *****
|
||||
|
||||
**Car**
|
||||
Which car (make, model, year) this fingerprint is for
|
||||
|
||||
**Route**
|
||||
A route with the fingerprint
|
||||
|
||||
-->
|
||||
|
||||
<!--- ***** Template: Car Bugfix *****
|
||||
|
||||
**Description**
|
||||
|
||||
A description of the bug and the fix. Also link the issue if it exists.
|
||||
|
||||
**Verification**
|
||||
|
||||
Explain how you tested this bug fix.
|
||||
|
||||
**Route**
|
||||
|
||||
Route: [a route with the bug fix]
|
||||
|
||||
|
||||
-->
|
||||
|
||||
<!--- ***** Template: Bugfix *****
|
||||
|
||||
**Description**
|
||||
|
||||
A description of the bug and the fix. Also link the issue if it exists.
|
||||
|
||||
**Verification**
|
||||
|
||||
Explain how you tested this bug fix.
|
||||
|
||||
|
||||
-->
|
||||
|
||||
<!--- ***** Template: Car Port *****
|
||||
|
||||
**Checklist**
|
||||
|
||||
- [ ] added entry to CAR in openpilot/selfdrive/car/*/values.py and ran `openpilot/selfdrive/car/docs.py` to generate new docs
|
||||
- [ ] test route added to [routes.py](https://github.com/commaai/openpilot/blob/master/openpilot/selfdrive/car/tests/routes.py)
|
||||
- [ ] route with openpilot:
|
||||
- [ ] route with stock system:
|
||||
- [ ] car harness used (if comma doesn't sell it, put N/A):
|
||||
|
||||
|
||||
-->
|
||||
|
||||
<!--- ***** Template: Refactor *****
|
||||
|
||||
**Description**
|
||||
|
||||
A description of the refactor, including the goals it accomplishes.
|
||||
|
||||
**Verification**
|
||||
|
||||
Explain how you tested the refactor for regressions.
|
||||
|
||||
|
||||
-->
|
||||
|
||||
@@ -40,6 +40,7 @@ jobs:
|
||||
echo "run-id=$run_id" >> "$GITHUB_OUTPUT"
|
||||
- name: Download diff
|
||||
if: steps.wait.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -48,7 +49,7 @@ jobs:
|
||||
name: diff_report_${{ github.event.number }}
|
||||
path: .
|
||||
- name: Comment on PR
|
||||
if: steps.wait.outcome == 'success'
|
||||
if: steps.wait.outcome == 'success' && hashFiles('diff_report.txt') != ''
|
||||
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b
|
||||
with:
|
||||
file-path: diff_report.txt
|
||||
|
||||
@@ -15,6 +15,11 @@ concurrency:
|
||||
group: docs-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
name: build docs
|
||||
@@ -30,8 +35,7 @@ jobs:
|
||||
- name: Build docs
|
||||
run: |
|
||||
git lfs pull
|
||||
pip install zensical
|
||||
python scripts/docs.py build
|
||||
python docs/serve.py --build
|
||||
|
||||
# Push to docs.comma.ai
|
||||
- uses: actions/checkout@v7
|
||||
@@ -52,7 +56,7 @@ jobs:
|
||||
git rm -rf .
|
||||
|
||||
# copy over docs
|
||||
cp -r ../docs_site/ docs/
|
||||
cp -r ../docs/_site/ docs/
|
||||
|
||||
# GitHub pages config
|
||||
touch docs/.nojekyll
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
name: "model review"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
paths:
|
||||
- 'openpilot/selfdrive/modeld/models/*.onnx'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'commaai/openpilot'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: true
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: master
|
||||
path: base
|
||||
- run: git lfs pull
|
||||
- run: cd base && git lfs pull
|
||||
|
||||
- name: scripts/reporter.py
|
||||
id: report
|
||||
run: |
|
||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "## Model Review" >> $GITHUB_OUTPUT
|
||||
PYTHONPATH=${{ github.workspace }} MASTER_PATH=${{ github.workspace }}/base python scripts/reporter.py >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Post model report comment
|
||||
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0
|
||||
with:
|
||||
header: model-review
|
||||
message: ${{ steps.report.outputs.content }}
|
||||
@@ -4,6 +4,11 @@ on:
|
||||
- cron: '0 9 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
|
||||
jobs:
|
||||
build___nightly:
|
||||
name: build __nightly
|
||||
|
||||
@@ -9,6 +9,9 @@ on:
|
||||
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
|
||||
jobs:
|
||||
package_updates:
|
||||
@@ -81,7 +84,7 @@ jobs:
|
||||
labels: bot
|
||||
|
||||
cleanup_closed_branches:
|
||||
if: github.repository == 'commaai/openpilot'
|
||||
if: github.repository == 'commaai/openpilot' && github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -20,7 +20,9 @@ concurrency:
|
||||
env:
|
||||
CI: 1
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
|
||||
jobs:
|
||||
build_release:
|
||||
@@ -125,10 +127,7 @@ jobs:
|
||||
timeout-minutes: ${{ contains(runner.name, 'nsc') && 2 || 999 }}
|
||||
env:
|
||||
RAYLIB_BACKEND: headless
|
||||
run: |
|
||||
# Pre-compile Python bytecode so each pytest worker doesn't need to
|
||||
$PYTEST --collect-only -m 'not slow' -qq
|
||||
MAX_EXAMPLES=1 $PYTEST -m 'not slow'
|
||||
run: tools/op.sh test
|
||||
|
||||
process_replay:
|
||||
name: process replay
|
||||
@@ -193,12 +192,6 @@ jobs:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: cd ${{ github.workspace }}/ci-artifacts && git push origin process-replay --force
|
||||
- name: Run regen
|
||||
if: false
|
||||
timeout-minutes: 4
|
||||
env:
|
||||
ONNXCPU: 1
|
||||
run: $PYTEST openpilot/selfdrive/test/process_replay/test_regen.py
|
||||
|
||||
simulator_driving:
|
||||
name: simulator driving
|
||||
@@ -219,7 +212,7 @@ jobs:
|
||||
env:
|
||||
# MetaDrive renders offscreen through panda3d's EGL pipe on llvmpipe
|
||||
EGL_PLATFORM: surfaceless
|
||||
run: pytest -s openpilot/tools/sim/tests/test_metadrive_bridge.py
|
||||
run: python openpilot/tools/sim/tests/test_metadrive_bridge.py
|
||||
|
||||
create_ui_report:
|
||||
name: Create UI Report
|
||||
|
||||
+4
-2
@@ -15,7 +15,7 @@ a.out
|
||||
.cache/
|
||||
bin/
|
||||
|
||||
# created at launch for TICI PYTHONPATH (PC uses editable installs via pyproject.toml)
|
||||
# created at launch for comma hardware PYTHONPATH (PC uses editable installs via pyproject.toml)
|
||||
/msgq
|
||||
/opendbc
|
||||
/rednose
|
||||
@@ -39,6 +39,8 @@ bin/
|
||||
*.os-*
|
||||
*.so
|
||||
*.a
|
||||
st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
|
||||
*.unchunked
|
||||
*.clb
|
||||
*.class
|
||||
*.pyxbldc
|
||||
@@ -54,7 +56,7 @@ compare_runtime*.html
|
||||
openpilot/selfdrive/modeld/models/tg_input_devices.json
|
||||
|
||||
# build artifacts
|
||||
docs_site/
|
||||
docs/_site/
|
||||
openpilot/selfdrive/pandad/pandad
|
||||
openpilot/cereal/services.h
|
||||
openpilot/cereal/gen
|
||||
|
||||
Vendored
+14
-14
@@ -30,14 +30,14 @@ export GIT_COMMIT=${env.GIT_COMMIT}
|
||||
export CI_ARTIFACTS_TOKEN=${env.CI_ARTIFACTS_TOKEN}
|
||||
export GITHUB_COMMENTS_TOKEN=${env.GITHUB_COMMENTS_TOKEN}
|
||||
export AZURE_TOKEN='${env.AZURE_TOKEN}'
|
||||
# only use 1 thread for tici tests since most require HIL
|
||||
# only use 1 thread since most require real hardware that can't be shared
|
||||
export PYTEST_ADDOPTS="-n0 -s"
|
||||
|
||||
|
||||
export GIT_SSH_COMMAND="ssh -i /data/gitkey"
|
||||
|
||||
source ~/.bash_profile
|
||||
if [ -f /TICI ]; then
|
||||
if [ -f /AGNOS ]; then
|
||||
source /etc/profile
|
||||
|
||||
rm -rf /tmp/tmp*
|
||||
@@ -206,35 +206,35 @@ node {
|
||||
deviceStage("onroad", "tizi-needs-can", ["UNSAFE=1"], [
|
||||
step("build openpilot", "cd openpilot/system/manager && ./build.py"),
|
||||
step("check dirty", "tools/release/check-dirty.sh"),
|
||||
step("onroad tests", "pytest openpilot/selfdrive/test/test_onroad.py -s", [timeout: 60]),
|
||||
step("onroad tests", "./openpilot/selfdrive/test/test_onroad.py", [timeout: 60]),
|
||||
])
|
||||
},
|
||||
'HW + Unit Tests': {
|
||||
deviceStage("tizi-hardware", "tizi-common", ["UNSAFE=1"], [
|
||||
step("build", "cd openpilot/system/manager && ./build.py"),
|
||||
step("test power draw", "pytest -s openpilot/selfdrive/test//test_power_draw.py"),
|
||||
step("test encoder", "pytest openpilot/system/loggerd/tests/test_encoder.py", [diffPaths: ["openpilot/system/loggerd/"]]),
|
||||
step("test manager", "pytest openpilot/system/manager/test/test_manager.py"),
|
||||
step("test power draw", "./openpilot/selfdrive/test/test_power_draw.py"),
|
||||
step("test encoder", "./openpilot/system/loggerd/tests/test_encoder.py", [diffPaths: ["openpilot/system/loggerd/"]]),
|
||||
step("test manager", "./openpilot/system/manager/test/test_manager.py"),
|
||||
])
|
||||
},
|
||||
'camerad OX03C10': {
|
||||
deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [
|
||||
step("build", "cd openpilot/system/manager && ./build.py"),
|
||||
step("test pandad", "pytest openpilot/selfdrive/pandad/tests/test_pandad.py"),
|
||||
step("test camerad", "pytest openpilot/system/camerad/test/test_camerad.py", [timeout: 90]),
|
||||
step("test pandad", "./openpilot/selfdrive/pandad/tests/test_pandad.py"),
|
||||
step("test camerad", "./openpilot/system/camerad/test/test_camerad.py", [timeout: 90]),
|
||||
])
|
||||
},
|
||||
'camerad OS04C10': {
|
||||
deviceStage("OS04C10", "tici-os04c10", ["UNSAFE=1"], [
|
||||
step("build", "cd openpilot/system/manager && ./build.py"),
|
||||
step("test pandad", "pytest openpilot/selfdrive/pandad/tests/test_pandad.py"),
|
||||
step("test camerad", "pytest openpilot/system/camerad/test/test_camerad.py", [timeout: 90]),
|
||||
step("test pandad", "./openpilot/selfdrive/pandad/tests/test_pandad.py"),
|
||||
step("test camerad", "./openpilot/system/camerad/test/test_camerad.py", [timeout: 90]),
|
||||
])
|
||||
},
|
||||
'sensord': {
|
||||
deviceStage("LSM + MMC", "tizi-lsmc", ["UNSAFE=1"], [
|
||||
step("build", "cd openpilot/system/manager && ./build.py"),
|
||||
step("test sensord", "pytest openpilot/system/sensord/tests/test_sensord.py"),
|
||||
step("test sensord", "./openpilot/system/sensord/tests/test_sensord.py"),
|
||||
])
|
||||
},
|
||||
'replay': {
|
||||
@@ -246,9 +246,9 @@ node {
|
||||
'tizi': {
|
||||
deviceStage("tizi", "tizi", ["UNSAFE=1"], [
|
||||
step("build openpilot", "cd openpilot/system/manager && ./build.py"),
|
||||
step("test pandad loopback", "pytest openpilot/selfdrive/pandad/tests/test_pandad_loopback.py"),
|
||||
step("test pandad spi", "pytest openpilot/selfdrive/pandad/tests/test_pandad_spi.py"),
|
||||
step("test amp", "pytest openpilot/common/hardware/tici/tests/test_amplifier.py"),
|
||||
step("test pandad loopback", "./openpilot/selfdrive/pandad/tests/test_pandad_loopback.py"),
|
||||
step("test pandad spi", "./openpilot/selfdrive/pandad/tests/test_pandad_spi.py"),
|
||||
step("test amp", "./openpilot/common/hardware/comma/tests/test_amplifier.py"),
|
||||
])
|
||||
},
|
||||
|
||||
|
||||
+11
-3
@@ -1,6 +1,14 @@
|
||||
Version 0.11.2 (2026-06-15)
|
||||
========================
|
||||
|
||||
Version 0.11.2 (2026-08-12)
|
||||
=======================
|
||||
* New driving model
|
||||
* Big model with 880M parameters
|
||||
* Support for big models running on an external GPU
|
||||
* Live stream cameras from comma connect
|
||||
* Generate dashcam clips from comma connect
|
||||
* Remote comma body control from comma connect
|
||||
* New alert sounds
|
||||
* CUPRA Born 2021-2023 support thanks to DaHansi!
|
||||
* Volkswagen ID.4 2021-2025 support thanks to DaHansi!
|
||||
|
||||
Version 0.11.1 (2026-05-18)
|
||||
========================
|
||||
|
||||
+19
-18
@@ -10,7 +10,7 @@ import numpy as np
|
||||
import SCons.Errors
|
||||
from SCons.Defaults import _stripixes
|
||||
|
||||
TICI = os.path.isfile('/TICI')
|
||||
COMMA_HARDWARE = os.path.isfile('/AGNOS')
|
||||
|
||||
SCons.Warnings.warningAsException(True)
|
||||
|
||||
@@ -24,7 +24,7 @@ release = not os.path.exists(File('#.gitattributes').abspath) # file absent on r
|
||||
AddOption('--minimal',
|
||||
action='store_false',
|
||||
dest='extras',
|
||||
default=(not TICI and not release),
|
||||
default=(not COMMA_HARDWARE and not release),
|
||||
help='the minimum build to run openpilot. no tests, tools, etc.')
|
||||
|
||||
submodule_python_paths = [
|
||||
@@ -46,22 +46,22 @@ if external_pythonpath := os.environ.get("PYTHONPATH"):
|
||||
arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
|
||||
if platform.system() == "Darwin":
|
||||
arch = "Darwin"
|
||||
elif arch == "aarch64" and TICI:
|
||||
arch = "larch64"
|
||||
elif arch == "aarch64" and COMMA_HARDWARE:
|
||||
arch = "comma_arm64"
|
||||
assert arch in [
|
||||
"larch64", # linux tici arm64
|
||||
"aarch64", # linux pc arm64
|
||||
"x86_64", # linux pc x64
|
||||
"Darwin", # macOS arm64 (x86 not supported)
|
||||
"comma_arm64", # linux comma hardware (AGNOS) arm64
|
||||
"aarch64", # linux pc arm64
|
||||
"x86_64", # linux pc x64
|
||||
"Darwin", # macOS arm64 (x86 not supported)
|
||||
]
|
||||
|
||||
pkg_names = ['acados', 'bzip2', 'capnproto', 'catch2', 'eigen', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd']
|
||||
pkg_names = ['acados', 'capnproto', 'eigen', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd']
|
||||
pkgs = [importlib.import_module(name) for name in pkg_names]
|
||||
acados = pkgs[pkg_names.index('acados')]
|
||||
ffmpeg = pkgs[pkg_names.index('ffmpeg')]
|
||||
# Shared package ships .so/.dylib; older device venvs still have static .a only.
|
||||
# Keep static link deps (x264/z/va/drm) when the installed package is static so
|
||||
# TICI CI works without upgrading the device venv yet.
|
||||
# COMMA_HARDWARE CI works without upgrading the device venv yet.
|
||||
# TODO: drop the static fallback once device venvs have comma-deps-ffmpeg>=7.1.0.post94
|
||||
_ffmpeg_lib_names = os.listdir(ffmpeg.LIB_DIR) if os.path.isdir(ffmpeg.LIB_DIR) else []
|
||||
ffmpeg_shared = any(
|
||||
@@ -129,10 +129,11 @@ env = Environment(
|
||||
CCFLAGS=[
|
||||
"-g",
|
||||
"-fPIC",
|
||||
"-pipe",
|
||||
"-O2",
|
||||
"-Wunused",
|
||||
"-Werror",
|
||||
"-Wshadow" if arch in ("Darwin", "larch64") else "-Wshadow=local",
|
||||
"-Wshadow" if arch in ("Darwin", "comma_arm64") else "-Wshadow=local",
|
||||
"-Wno-unknown-warning-option",
|
||||
"-Wno-inconsistent-missing-override",
|
||||
"-Wno-c99-designator",
|
||||
@@ -164,24 +165,24 @@ env = Environment(
|
||||
COMPILATIONDB_USE_ABSPATH=True,
|
||||
REDNOSE_ROOT="#rednose_repo",
|
||||
tools=["default", "cython", "compilation_db", "rednose_filter"],
|
||||
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
|
||||
toolpath=["#msgq_repo/site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
|
||||
)
|
||||
# SCons' Darwin linker tool doesn't define the variables used to expand RPATH.
|
||||
if arch == "Darwin":
|
||||
env["RPATHPREFIX"] = "-Wl,-rpath,"
|
||||
env["RPATHSUFFIX"] = ""
|
||||
env["_RPATH"] = "${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}"
|
||||
if arch != "larch64":
|
||||
if arch != "comma_arm64":
|
||||
env['_LIBFLAGS'] = _libflags
|
||||
|
||||
# Arch-specific flags and paths
|
||||
if arch == "larch64":
|
||||
if arch == "comma_arm64":
|
||||
env["CC"] = "clang"
|
||||
env["CXX"] = "clang++"
|
||||
env.Append(LIBPATH=[
|
||||
"/usr/lib/aarch64-linux-gnu",
|
||||
])
|
||||
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57", "-DQCOM2"]
|
||||
arch_flags = ["-D__COMMA_HARDWARE__", "-mcpu=cortex-a57"]
|
||||
env.Append(CCFLAGS=arch_flags)
|
||||
env.Append(CXXFLAGS=arch_flags)
|
||||
elif arch == "Darwin":
|
||||
@@ -233,7 +234,7 @@ Export('envCython', 'np_version')
|
||||
Export('env', 'arch', 'acados', 'release', 'ffmpeg_libs')
|
||||
|
||||
# Setup cache dir
|
||||
default_cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache'
|
||||
default_cache_dir = '/data/scons_cache' if arch == "comma_arm64" else '/tmp/scons_cache'
|
||||
cache_dir = ARGUMENTS.get('cache_dir', default_cache_dir)
|
||||
cache_size_limit = 4e9 if "CI" in os.environ else 2e9
|
||||
CacheDir(cache_dir)
|
||||
@@ -280,7 +281,7 @@ SConscript([
|
||||
'openpilot/system/loggerd/SConscript',
|
||||
])
|
||||
|
||||
if arch == "larch64":
|
||||
if arch == "comma_arm64":
|
||||
SConscript(['openpilot/system/camerad/SConscript'])
|
||||
|
||||
# Build selfdrive
|
||||
@@ -295,7 +296,7 @@ SConscript([
|
||||
SConscript(['openpilot/sunnypilot/SConscript'])
|
||||
|
||||
# Build desktop-only tools
|
||||
if GetOption('extras') and arch != "larch64":
|
||||
if GetOption('extras') and arch != "comma_arm64":
|
||||
SConscript([
|
||||
'openpilot/tools/replay/SConscript',
|
||||
'openpilot/tools/cabana/SConscript',
|
||||
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
import contextlib
|
||||
import gc
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.system.manager import manager
|
||||
from openpilot.common.hardware import TICI, HARDWARE
|
||||
|
||||
# these are heavy CI-only tests, invoked explicitly in .github/workflows/tests.yaml
|
||||
collect_ignore = [
|
||||
"openpilot/selfdrive/test/process_replay/test_processes.py",
|
||||
"openpilot/selfdrive/test/process_replay/test_regen.py",
|
||||
|
||||
"openpilot/tools/sim/",
|
||||
|
||||
# tinygrad JIT has process-global state. Other test files import modeld → tinygrad,
|
||||
# which corrupts JIT captures for test_warp.py in the same process. Run separately in CI.
|
||||
"openpilot/sunnypilot/modeld_v2/tests/test_warp.py",
|
||||
]
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
# TODO: fix tests and enable test order randomization
|
||||
if session.config.pluginmanager.hasplugin('randomly'):
|
||||
session.config.option.randomly_reorganize = False
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
||||
def pytest_runtest_call(item):
|
||||
# ensure we run as a hook after capturemanager's
|
||||
if item.get_closest_marker("nocapture") is not None:
|
||||
capmanager = item.config.pluginmanager.getplugin("capturemanager")
|
||||
with capmanager.global_and_fixture_disabled():
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def clean_env():
|
||||
starting_env = dict(os.environ)
|
||||
yield
|
||||
os.environ.clear()
|
||||
os.environ.update(starting_env)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def openpilot_function_fixture(request):
|
||||
with clean_env():
|
||||
# setup a clean environment for each test
|
||||
with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix:
|
||||
prefix = os.environ["OPENPILOT_PREFIX"]
|
||||
|
||||
yield
|
||||
|
||||
# ensure the test doesn't change the prefix
|
||||
assert "OPENPILOT_PREFIX" in os.environ and prefix == os.environ["OPENPILOT_PREFIX"]
|
||||
|
||||
# cleanup any started processes
|
||||
manager.manager_cleanup()
|
||||
|
||||
# some processes disable gc for performance, re-enable here
|
||||
if not gc.isenabled():
|
||||
gc.enable()
|
||||
gc.collect()
|
||||
|
||||
# If you use setUpClass, the environment variables won't be cleared properly,
|
||||
# so we need to hook both the function and class pytest fixtures
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def openpilot_class_fixture():
|
||||
with clean_env():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def tici_setup_fixture(request, openpilot_function_fixture):
|
||||
"""Ensure a consistent state for tests on-device. Needs the openpilot function fixture to run first."""
|
||||
if 'skip_tici_setup' in request.keywords:
|
||||
return
|
||||
HARDWARE.initialize_hardware()
|
||||
HARDWARE.set_power_save(False)
|
||||
os.system("pkill -9 -f athena")
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
skipper = pytest.mark.skip(reason="Skipping tici test on PC")
|
||||
for item in items:
|
||||
if "tici" in item.keywords:
|
||||
if not TICI:
|
||||
item.add_marker(skipper)
|
||||
else:
|
||||
item.fixturenames.append('tici_setup_fixture')
|
||||
|
||||
if "xdist_group_class_property" in item.keywords:
|
||||
class_property_name = item.get_closest_marker('xdist_group_class_property').args[0]
|
||||
class_property_value = getattr(item.cls, class_property_name)
|
||||
item.add_marker(pytest.mark.xdist_group(class_property_value))
|
||||
@@ -61,7 +61,7 @@ A good pull request has all of the following:
|
||||
|
||||
* Report bugs in GitHub issues.
|
||||
* Report driving issues in the `#driving-feedback` Discord channel.
|
||||
* Consider opting into driver camera uploads to improve the driver monitoring model.
|
||||
* Consider opting into cabin camera uploads to improve the driver monitoring model.
|
||||
* Connect your device to Wi-Fi regularly, so that we can pull data for training better driving models.
|
||||
* Run the `nightly` branch and report issues. This branch is like `master` but it's built just like a release.
|
||||
* Annotate images in the [comma10k dataset](https://github.com/commaai/comma10k).
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Docs development
|
||||
|
||||
The `docs/` tree is the source for [docs.comma.ai](https://docs.comma.ai).
|
||||
The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
|
||||
|
||||
Those commands must be run in the root directory of openpilot, **not /docs**
|
||||
|
||||
**1. Install the docs dependencies**
|
||||
``` bash
|
||||
uv pip install .[docs]
|
||||
```
|
||||
|
||||
**2. Build the new site**
|
||||
``` bash
|
||||
docs build
|
||||
```
|
||||
|
||||
**3. Run the new site locally**
|
||||
``` bash
|
||||
docs serve
|
||||
```
|
||||
|
||||
References:
|
||||
* https://zensical.org/docs/
|
||||
+2
-2
@@ -52,7 +52,7 @@ Many factors can impact the performance of openpilot DM, causing it to be unable
|
||||
|
||||
* Low light conditions, such as driving at night or in dark tunnels.
|
||||
* Bright light (due to oncoming headlights, direct sunlight, etc.).
|
||||
* The driver's face is partially or completely outside field of view of the driver facing camera.
|
||||
* The driver facing camera is obstructed, covered, or damaged.
|
||||
* The driver's face is partially or completely outside field of view of the cabin camera.
|
||||
* The cabin camera is obstructed, covered, or damaged.
|
||||
|
||||
The list above does not represent an exhaustive list of situations that may interfere with proper operation of openpilot components. A driver should not rely on openpilot DM to assess their level of attention.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Docs development
|
||||
|
||||
The `docs/` tree is the source for [docs.comma.ai](https://docs.comma.ai).
|
||||
The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
|
||||
|
||||
**1. Build the site**
|
||||
``` bash
|
||||
python docs/serve.py --build
|
||||
```
|
||||
|
||||
**2. Run the site locally** (rebuilds on change)
|
||||
``` bash
|
||||
python docs/serve.py
|
||||
```
|
||||
@@ -1 +1 @@
|
||||
../../selfdrive/assets/icons_mici/settings/comma_icon.png
|
||||
../../openpilot/selfdrive/assets/icons_mici/settings/comma_icon.png
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9e019ed5af500e8820d05934d47e0380728e1b30e01a179f786dc4edb1eccbd7
|
||||
size 349
|
||||
@@ -1,3 +0,0 @@
|
||||
# openpilot glossary
|
||||
|
||||
{{GLOSSARY_DEFINITIONS}}
|
||||
@@ -10,13 +10,13 @@ For each segment, openpilot records the following log types:
|
||||
|
||||
rlogs contain all the messages passed amongst openpilot's processes. See [openpilot/cereal/services.py](https://github.com/commaai/openpilot/blob/master/openpilot/cereal/services.py) for a list of all the logged services. They're a zstd archive of the serialized [Cap’n Proto](https://capnproto.org/) messages.
|
||||
|
||||
## {f,e,d}camera.hevc
|
||||
## camera video files
|
||||
|
||||
Each camera stream is H.265 encoded and written to its respective file.
|
||||
|
||||
* `fcamera.hevc` is the road camera
|
||||
* `fcamera.hevc` is the narrow road camera (the main forward camera)
|
||||
* `ecamera.hevc` is the wide road camera
|
||||
* `dcamera.hevc` is the driver camera
|
||||
* `dcamera.hevc` is the cabin camera
|
||||
|
||||
## qlog.zst & qcamera.ts
|
||||
|
||||
|
||||
@@ -21,14 +21,14 @@ In general, driver monitoring feedback is very actionable, and we can fix your c
|
||||
To post your feedback:
|
||||
|
||||
1. Join the [community Discord](https://discord.comma.ai).
|
||||
2. If driver camera recording is toggled off, temporarily enable driver camera recording in the settings until you reproduce the issue.
|
||||
3. Using comma connect, identify the relevant segment and upload the segment's logs and driver camera.
|
||||
2. If cabin camera recording is toggled off, temporarily enable cabin camera recording in the settings until you reproduce the issue.
|
||||
3. Using comma connect, identify the relevant segment and upload the segment's logs and cabin camera.
|
||||
4. Post the segment in the `#openpilot-experience` channel on Discord with a good description.
|
||||
|
||||
Before posting feedback, please ensure:
|
||||
|
||||
- **openpilot is up to date** you should be on the latest openpilot release or nightly
|
||||
- **the driver camera has a clear view of the driver** ensure nothing blocks view of the driver (e.g. a cable), the lens is clean, etc.
|
||||
- **the cabin camera has a clear view of the driver** ensure nothing blocks view of the driver (e.g. a cable), the lens is clean, etc.
|
||||
- **your device is mounted properly** your device must be mounted horizontally center and relatively high on the windshield
|
||||
|
||||
## Other bugs
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
import posixpath
|
||||
import re
|
||||
import tomllib
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
from markdown.extensions import Extension
|
||||
from markdown.preprocessors import Preprocessor
|
||||
from markdown.treeprocessors import Treeprocessor
|
||||
|
||||
from zensical.extensions.links import LinksTreeprocessor
|
||||
|
||||
GlossaryTerm = tuple[str, re.Pattern[str], str]
|
||||
|
||||
GLOSSARY_FILE = Path(__file__).with_name("glossary.toml")
|
||||
GLOSSARY_PAGE = "concepts/glossary.md"
|
||||
GLOSSARY_PLACEHOLDER = "{{GLOSSARY_DEFINITIONS}}"
|
||||
|
||||
SKIP_TAGS = {
|
||||
"a",
|
||||
"code",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"kbd",
|
||||
"pre",
|
||||
"script",
|
||||
"style",
|
||||
}
|
||||
|
||||
def clean_tooltip(description: str) -> str:
|
||||
text = re.sub(r"\[([^\]]+)]\([^)]+\)", r"\1", description)
|
||||
text = re.sub(r"`([^`]+)`", r"\1", text)
|
||||
text = re.sub(r"[*_~]", "", text)
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
|
||||
def load_glossary() -> tuple[list[GlossaryTerm], str]:
|
||||
with GLOSSARY_FILE.open("rb") as f:
|
||||
glossary_data = tomllib.load(f).get("glossary", {})
|
||||
|
||||
glossary: list[GlossaryTerm] = []
|
||||
rendered = []
|
||||
for key, value in glossary_data.items():
|
||||
label = str(key).strip().replace("_", " ")
|
||||
description = str(value).strip()
|
||||
if not description:
|
||||
continue
|
||||
|
||||
slug = label.replace(" ", "-").replace("_", "-").lower()
|
||||
glossary.append((slug, re.compile(rf"(?<!\w){re.escape(label)}(?!\w)", re.IGNORECASE), clean_tooltip(description)))
|
||||
rendered.append(f'* <span id="{slug}"></span>**{label}**: {description}')
|
||||
|
||||
return glossary, "\n".join(rendered)
|
||||
|
||||
|
||||
class GlossaryPreprocessor(Preprocessor):
|
||||
def __init__(self, md, glossary: str):
|
||||
super().__init__(md)
|
||||
self.glossary = glossary
|
||||
|
||||
def run(self, lines: list[str]) -> list[str]:
|
||||
markdown = "\n".join(lines)
|
||||
if GLOSSARY_PLACEHOLDER not in markdown:
|
||||
return lines
|
||||
return markdown.replace(GLOSSARY_PLACEHOLDER, self.glossary).splitlines()
|
||||
|
||||
|
||||
class GlossaryTreeprocessor(Treeprocessor):
|
||||
def __init__(self, md, glossary: list[GlossaryTerm]):
|
||||
super().__init__(md)
|
||||
self.glossary = glossary
|
||||
self.seen: set[str] = set()
|
||||
|
||||
def run(self, root: ET.Element) -> None:
|
||||
at = self.md.treeprocessors.get_index_for_name("zrelpath")
|
||||
processor = self.md.treeprocessors[at]
|
||||
if not isinstance(processor, LinksTreeprocessor):
|
||||
raise TypeError("Links processor not registered")
|
||||
if processor.path == GLOSSARY_PAGE:
|
||||
return
|
||||
|
||||
self.seen.clear()
|
||||
glossary_href = f"{posixpath.relpath(GLOSSARY_PAGE, posixpath.dirname(processor.path) or '.')}#"
|
||||
self._walk(root, glossary_href)
|
||||
|
||||
def _walk(self, element: ET.Element, glossary_href: str) -> None:
|
||||
if element.tag in SKIP_TAGS or element.attrib.get("data-glossary-skip") is not None:
|
||||
return
|
||||
|
||||
self._replace(element, glossary_href)
|
||||
|
||||
idx = 0
|
||||
while idx < len(element):
|
||||
child = element[idx]
|
||||
self._walk(child, glossary_href)
|
||||
idx = self._replace(element, glossary_href, idx) + 1
|
||||
|
||||
def _replace(self, parent: ET.Element, glossary_href: str, index: int | None = None) -> int:
|
||||
child = None if index is None else parent[index]
|
||||
text = parent.text if child is None else child.tail
|
||||
pieces = self._pieces(text or "", glossary_href)
|
||||
if not pieces:
|
||||
return -1 if index is None else index
|
||||
|
||||
if child is None:
|
||||
parent.text = pieces[0] if isinstance(pieces[0], str) else ""
|
||||
# Insert replacements for parent.text before the first existing child.
|
||||
insert_at = -1
|
||||
else:
|
||||
assert index is not None
|
||||
child.tail = pieces[0] if isinstance(pieces[0], str) else ""
|
||||
insert_at = index
|
||||
|
||||
start = 1 if isinstance(pieces[0], str) else 0
|
||||
previous = child
|
||||
|
||||
for piece in pieces[start:]:
|
||||
if isinstance(piece, str):
|
||||
previous.tail = (previous.tail or "") + piece
|
||||
continue
|
||||
|
||||
insert_at += 1
|
||||
parent.insert(insert_at, piece)
|
||||
previous = piece
|
||||
|
||||
return insert_at
|
||||
|
||||
def _pieces(self, text: str, glossary_href: str) -> list[str | ET.Element]:
|
||||
if not text.strip():
|
||||
return []
|
||||
|
||||
pieces: list[str | ET.Element] = []
|
||||
cursor = 0
|
||||
|
||||
while True:
|
||||
best = None
|
||||
for slug, pattern, tooltip in self.glossary:
|
||||
if slug in self.seen:
|
||||
continue
|
||||
|
||||
found = pattern.search(text, cursor)
|
||||
if found is None:
|
||||
continue
|
||||
|
||||
candidate = (slug, tooltip, found.start(), found.end())
|
||||
if best is None:
|
||||
best = candidate
|
||||
continue
|
||||
|
||||
_, _, best_start, best_end = best
|
||||
_, _, current_start, current_end = candidate
|
||||
if current_start < best_start:
|
||||
best = candidate
|
||||
continue
|
||||
|
||||
if current_start == best_start and current_end - current_start > best_end - best_start:
|
||||
best = candidate
|
||||
|
||||
if best is None:
|
||||
break
|
||||
|
||||
slug, tooltip, start, end = best
|
||||
if start > cursor:
|
||||
pieces.append(text[cursor:start])
|
||||
|
||||
link = ET.Element(
|
||||
"a",
|
||||
{
|
||||
"class": "glossary-term",
|
||||
"data-glossary-term": "",
|
||||
"href": f"{glossary_href}{slug}",
|
||||
},
|
||||
)
|
||||
ET.SubElement(link, "span", {"class": "glossary-term__label"}).text = text[start:end]
|
||||
ET.SubElement(
|
||||
link,
|
||||
"span",
|
||||
{
|
||||
"class": "glossary-term__tooltip",
|
||||
"data-search-exclude": "",
|
||||
},
|
||||
).text = tooltip
|
||||
pieces.append(link)
|
||||
self.seen.add(slug)
|
||||
cursor = end
|
||||
|
||||
if not pieces:
|
||||
return []
|
||||
if cursor < len(text):
|
||||
pieces.append(text[cursor:])
|
||||
return pieces
|
||||
|
||||
|
||||
class GlossaryExtension(Extension):
|
||||
def extendMarkdown(self, md) -> None:
|
||||
md.registerExtension(self)
|
||||
glossary, rendered = load_glossary()
|
||||
|
||||
md.preprocessors.register(
|
||||
GlossaryPreprocessor(md, rendered),
|
||||
"docs-ext-glossary-preprocessor",
|
||||
27,
|
||||
)
|
||||
md.treeprocessors.register(
|
||||
GlossaryTreeprocessor(md, glossary),
|
||||
"docs-ext-glossary-treeprocessor",
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
def makeExtension(**kwargs) -> GlossaryExtension:
|
||||
return GlossaryExtension(**kwargs)
|
||||
@@ -1,8 +0,0 @@
|
||||
[glossary]
|
||||
onroad = "openpilot's system state while ignition is on."
|
||||
offroad = "openpilot's system state while ignition is off."
|
||||
route = "A route is a recording of an onroad session."
|
||||
segment = "Routes are split into one minute chunks called segments."
|
||||
"comma connect" = "The web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai)."
|
||||
panda = "The secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda)."
|
||||
"comma four" = "The latest hardware by comma.ai for running openpilot. More info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four)."
|
||||
+555
@@ -0,0 +1,555 @@
|
||||
import argparse
|
||||
import functools
|
||||
import html
|
||||
import http.server
|
||||
import json
|
||||
import posixpath
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
DOCS_DIR = Path(__file__).resolve().parent
|
||||
SITE_DIR = DOCS_DIR / "_site"
|
||||
TEMPLATE_FILE = DOCS_DIR / "template.html"
|
||||
EXCLUDE_DIRS = {"_site", "__pycache__"}
|
||||
|
||||
REPO_URL = "https://github.com/commaai/openpilot/"
|
||||
|
||||
# (title, target) pairs. target is a page path or an absolute URL.
|
||||
# A None target marks a section header.
|
||||
NAV: list[tuple[str, str | None]] = [
|
||||
("What is openpilot?", "index.md"),
|
||||
("How-to", None),
|
||||
("Turn the speed blue", "how-to/turn-the-speed-blue.md"),
|
||||
("Connect to a comma 3X or four", "how-to/connect-to-comma.md"),
|
||||
("Add support for a car", "how-to/car-port.md"),
|
||||
("Concepts", None),
|
||||
("Logs", "concepts/logs.md"),
|
||||
("Safety", "concepts/safety.md"),
|
||||
("Glossary", "concepts/glossary.md"),
|
||||
("Contributing", None),
|
||||
("Feedback", "contributing/feedback.md"),
|
||||
("Roadmap", "contributing/roadmap.md"),
|
||||
("Contributing Guide →", "https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md"),
|
||||
("Links", None),
|
||||
("Blog →", "https://blog.comma.ai"),
|
||||
("Bounties →", "https://comma.ai/bounties"),
|
||||
("GitHub →", "https://github.com/commaai"),
|
||||
("Discord →", "https://discord.comma.ai"),
|
||||
("X →", "https://x.com/comma_ai"),
|
||||
]
|
||||
|
||||
GLOSSARY_DESCRIPTIONS = {
|
||||
"onroad": "openpilot's system state while ignition is on.",
|
||||
"offroad": "openpilot's system state while ignition is off.",
|
||||
"route": "A route is a recording of an onroad session.",
|
||||
"segment": "Routes are split into one minute chunks called segments.",
|
||||
"comma connect": "The web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai).",
|
||||
"panda": "The secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda).",
|
||||
"comma four": "The latest hardware by comma.ai for running openpilot. More info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four).",
|
||||
}
|
||||
GLOSSARY_PAGE = "concepts/glossary.md"
|
||||
GLOSSARY_ROUTE = GLOSSARY_PAGE.removesuffix(".md")
|
||||
GLOSSARY_SKIP = frozenset("a code h1 h2 h3 h4 h5 h6 kbd pre script style".split())
|
||||
|
||||
_ENTITY = re.compile(r"&(?:#x?[0-9a-fA-F]+|[a-zA-Z]+);")
|
||||
_LIST = re.compile(r"^(\s*)([*+-]|\d+\.)\s+(.*)$")
|
||||
_HEADING = re.compile(r"^(#{1,6})\s+(.*)$")
|
||||
_HR = re.compile(r"^(-{3,}|\*{3,}|_{3,})$")
|
||||
_ATTR_URL = re.compile(r"""(?P<pre>\b(?:href|src)=(?P<q>["']))(?P<url>.*?)(?P=q)""")
|
||||
_VOID = frozenset("br img hr meta link input".split())
|
||||
_URL = re.compile(r"https?://[^\s<>\[\]\"']+")
|
||||
_ADMONITION = re.compile(r"^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$", re.I)
|
||||
|
||||
|
||||
def page_route(path: str) -> str:
|
||||
path = path.removesuffix(".md")
|
||||
return posixpath.dirname(path) or "." if posixpath.basename(path) == "index" else path
|
||||
|
||||
|
||||
def page_href(current: str, target: str) -> str:
|
||||
route = posixpath.relpath(page_route(target), page_route(current))
|
||||
return ("." if route == "." else route) + "/"
|
||||
|
||||
|
||||
def rewrite_relative_url(value: str, page: str) -> str | None:
|
||||
url = urllib.parse.urlparse(value)
|
||||
if value.startswith(("#", "/")) or url.scheme or url.netloc or not url.path:
|
||||
return None
|
||||
target = posixpath.normpath(posixpath.join(posixpath.dirname(page), url.path))
|
||||
if target == ".." or target.startswith("../"):
|
||||
return None
|
||||
path = page_href(page, target) if target.endswith(".md") else posixpath.relpath(target, page_route(page))
|
||||
return url._replace(path=path).geturl()
|
||||
|
||||
|
||||
def rewrite_html_urls(fragment: str, page: str) -> str:
|
||||
def repl(m: re.Match[str]) -> str:
|
||||
r = rewrite_relative_url(m.group("url"), page)
|
||||
return m.group(0) if r is None else f'{m.group("pre")}{r}{m.group("q")}'
|
||||
|
||||
return _ATTR_URL.sub(repl, fragment)
|
||||
|
||||
|
||||
def esc(text: str, attr: bool = False) -> str:
|
||||
held: list[str] = []
|
||||
|
||||
def hold(m: re.Match[str]) -> str:
|
||||
held.append(m.group(0))
|
||||
return f"\0{len(held) - 1}\0"
|
||||
|
||||
return re.sub(r"\0(\d+)\0", lambda m: held[int(m.group(1))], html.escape(_ENTITY.sub(hold, text), quote=attr))
|
||||
|
||||
|
||||
def clean_tooltip(description: str) -> str:
|
||||
text = re.sub(r"\[([^\]]+)]\([^)]+\)", r"\1", description)
|
||||
return re.sub(r"\s+", " ", re.sub(r"[*_~]", "", re.sub(r"`([^`]+)`", r"\1", text))).strip()
|
||||
|
||||
|
||||
def glossary_slug(label: str) -> str:
|
||||
return label.replace(" ", "-").replace("_", "-").lower()
|
||||
|
||||
|
||||
GLOSSARY_TERMS = [(glossary_slug(l), re.compile(rf"(?<!\w){re.escape(l)}(?!\w)", re.I), clean_tooltip(d)) for l, d in GLOSSARY_DESCRIPTIONS.items()]
|
||||
GLOSSARY_DEFINITIONS = "\n".join(f'* <span id="{glossary_slug(l)}"></span>**{l}**: {d}' for l, d in GLOSSARY_DESCRIPTIONS.items())
|
||||
|
||||
|
||||
def inject_glossary(body: str, page: str) -> str:
|
||||
if page == GLOSSARY_PAGE:
|
||||
return body
|
||||
route = "." if page == "index.md" else page.removesuffix(".md")
|
||||
base, seen, out, skip, depth = f"{posixpath.relpath(GLOSSARY_ROUTE, route)}/#", set(), [], None, 0
|
||||
for part in re.split(r"(<[^>]+>)", body):
|
||||
if not part:
|
||||
continue
|
||||
if part.startswith("<"):
|
||||
out.append(part)
|
||||
if part.startswith("<!") or not (m := re.match(r"</?\s*([a-zA-Z0-9]+)", part)):
|
||||
continue
|
||||
tag = m.group(1).lower()
|
||||
if tag not in GLOSSARY_SKIP:
|
||||
continue
|
||||
closing, void = part.startswith("</"), part.endswith("/>") or tag in _VOID
|
||||
if closing and skip == tag and depth:
|
||||
depth -= 1
|
||||
skip = None if not depth else skip
|
||||
elif not closing and not void:
|
||||
skip, depth = (tag, 1) if skip is None else (skip, depth + (skip == tag))
|
||||
continue
|
||||
if depth:
|
||||
out.append(part)
|
||||
continue
|
||||
cur, text = 0, part
|
||||
while True:
|
||||
best = None
|
||||
for order, (slug, pat, tip) in enumerate(GLOSSARY_TERMS):
|
||||
if slug in seen or (found := pat.search(text, cur)) is None:
|
||||
continue
|
||||
cand = (found.start(), found.start() - found.end(), order, slug, tip, found.end(), found.group(0))
|
||||
if best is None or cand[:3] < best[:3]:
|
||||
best = cand
|
||||
if best is None:
|
||||
out.append(text[cur:])
|
||||
break
|
||||
start, _, _, slug, tip, end, matched = best
|
||||
out.append(text[cur:start])
|
||||
out.append(
|
||||
f'<a class="glossary-term" data-glossary-term="" href="{base}{slug}">'
|
||||
+ f'<span class="glossary-term__label">{matched}</span>'
|
||||
+ f'<span class="glossary-term__tooltip" data-search-exclude="">{esc(tip)}</span></a>'
|
||||
)
|
||||
seen.add(slug)
|
||||
cur = end
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def slugify(text: str) -> str:
|
||||
text = html.unescape(re.sub(r"<[^>]+>", "", text)).lower()
|
||||
return re.sub(r"[-\s]+", "-", re.sub(r"[^\w\s-]", "", text, flags=re.UNICODE)).strip("-")
|
||||
|
||||
|
||||
def _parse_link(text: str, start: int) -> tuple[str, str, int] | None:
|
||||
if start >= len(text) or text[start] != "[":
|
||||
return None
|
||||
depth, i = 0, start
|
||||
while i < len(text):
|
||||
depth += (text[i] == "[") - (text[i] == "]")
|
||||
if text[i] == "]" and depth == 0:
|
||||
label = text[start + 1 : i]
|
||||
if i + 1 >= len(text) or text[i + 1] != "(":
|
||||
return None
|
||||
j, dp = i + 2, 1
|
||||
while j < len(text) and dp:
|
||||
dp += (text[j] == "(") - (text[j] == ")")
|
||||
j += 1
|
||||
return None if dp else (label, text[i + 2 : j - 1], j)
|
||||
i += 1
|
||||
return None
|
||||
|
||||
|
||||
def autolink_plain(text: str) -> str:
|
||||
parts: list[str] = []
|
||||
last = 0
|
||||
for m in _URL.finditer(text):
|
||||
start = m.start()
|
||||
if start > 0 and text[start - 1].isalnum():
|
||||
continue
|
||||
parts.append(esc(text[last:start]))
|
||||
url = m.group(0).rstrip(".,;:!?)]")
|
||||
parts.append(f'<a href="{esc(url, True)}">{esc(url)}</a>')
|
||||
last = start + len(url)
|
||||
parts.append(esc(text[last:]))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def render_inline(text: str, page: str) -> str:
|
||||
out, i, n = [], 0, len(text)
|
||||
while i < n:
|
||||
if text[i] == "\n" and i >= 2 and text[i - 2 : i] == " " and out and out[-1].endswith(" "):
|
||||
out[-1] = out[-1][:-2]
|
||||
out.append("<br>\n")
|
||||
i += 1
|
||||
continue
|
||||
if text[i] == "`" and (end := text.find("`", i + 1)) != -1:
|
||||
out.append(f"<code>{esc(text[i + 1 : end])}</code>")
|
||||
i = end + 1
|
||||
continue
|
||||
if text[i] == "!" and i + 1 < n and text[i + 1] == "[" and (p := _parse_link(text, i + 1)):
|
||||
label, url, end = p
|
||||
src = rewrite_relative_url(url, page) or url
|
||||
out.append(f'<img alt="{esc(label, True)}" src="{esc(src, True)}">')
|
||||
i = end
|
||||
continue
|
||||
if text[i] == "[" and (p := _parse_link(text, i)):
|
||||
label, url, end = p
|
||||
href = rewrite_relative_url(url, page) or url
|
||||
out.append(f'<a href="{esc(href, True)}">{render_inline(label, page)}</a>')
|
||||
i = end
|
||||
continue
|
||||
if text[i] == "<":
|
||||
if text.startswith("<!--", i):
|
||||
end = text.find("-->", i + 4)
|
||||
end = n if end < 0 else end + 3
|
||||
out.append(rewrite_html_urls(text[i:end], page))
|
||||
i = end
|
||||
continue
|
||||
if m := re.match(r"<[^>]+>", text[i:]):
|
||||
out.append(rewrite_html_urls(m.group(0), page))
|
||||
i += len(m.group(0))
|
||||
continue
|
||||
if (text.startswith("**", i) or text.startswith("__", i)) and (end := text.find(text[i : i + 2], i + 2)) != -1:
|
||||
out.append(f"<strong>{render_inline(text[i + 2 : end], page)}</strong>")
|
||||
i = end + 2
|
||||
continue
|
||||
if text[i] in "*_" and i + 1 < n and text[i + 1] not in " \t\n" and (end := text.find(text[i], i + 1)) > i + 1:
|
||||
out.append(f"<em>{render_inline(text[i + 1 : end], page)}</em>")
|
||||
i = end + 1
|
||||
continue
|
||||
j = i + 1
|
||||
while j < n and text[j] not in "`[<!*_":
|
||||
j += 1
|
||||
out.append(autolink_plain(text[i:j]))
|
||||
i = j
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _trow(line: str) -> list[str]:
|
||||
return [c.strip() for c in line.strip().removeprefix("|").removesuffix("|").split("|")]
|
||||
|
||||
|
||||
def _is_sep(line: str) -> bool:
|
||||
return "|" in line and all(re.fullmatch(r":?-{3,}:?", c.strip()) for c in _trow(line))
|
||||
|
||||
|
||||
def _align(sep: str) -> str:
|
||||
s = sep.strip()
|
||||
if s.startswith(":") and s.endswith(":"):
|
||||
return ' style="text-align: center;"'
|
||||
if s.endswith(":"):
|
||||
return ' style="text-align: right;"'
|
||||
if s.startswith(":"):
|
||||
return ' style="text-align: left;"'
|
||||
return ""
|
||||
|
||||
|
||||
def _list_info(line: str) -> tuple[int, str, str] | None:
|
||||
m = _LIST.match(line)
|
||||
return None if not m else (len(m.group(1)) // 4, "ol" if m.group(2)[-1] == "." else "ul", m.group(3))
|
||||
|
||||
|
||||
def _render_blocks(text: str, page: str) -> str:
|
||||
lines, out, i, n = text.splitlines(), [], 0, 0
|
||||
n = len(lines)
|
||||
while i < n:
|
||||
line, s = lines[i], lines[i].strip()
|
||||
if not s:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if s.startswith("```"):
|
||||
lang, body = s[3:].strip(), []
|
||||
i += 1
|
||||
while i < n and not lines[i].strip().startswith("```"):
|
||||
body.append(lines[i])
|
||||
i += 1
|
||||
if i < n:
|
||||
i += 1
|
||||
code = html.escape("\n".join(body) + ("\n" if body else ""))
|
||||
cls = f' class="language-{html.escape(lang)}"' if lang else ""
|
||||
out.append(f"<pre><code{cls}>{code}</code></pre>")
|
||||
continue
|
||||
|
||||
if m := _HEADING.match(s):
|
||||
content, level = m.group(2).rstrip("#").strip(), len(m.group(1))
|
||||
sid = slugify(content)
|
||||
out.append(f'<h{level} id="{sid}">{render_inline(content, page)}<a class="headerlink" href="#{sid}" title="Permanent link">#</a></h{level}>')
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if _HR.fullmatch(s):
|
||||
out.append("<hr>")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if "|" in line and i + 1 < n and _is_sep(lines[i + 1]):
|
||||
headers, aligns = _trow(line), [_align(c) for c in _trow(lines[i + 1])]
|
||||
i += 2
|
||||
rows = []
|
||||
while i < n and "|" in lines[i] and lines[i].strip():
|
||||
rows.append(_trow(lines[i]))
|
||||
i += 1
|
||||
parts = (
|
||||
["<table>", "<thead>", "<tr>"]
|
||||
+ [f"<th{aligns[j] if j < len(aligns) else ''}>{render_inline(h, page)}</th>" for j, h in enumerate(headers)]
|
||||
+ ["</tr>", "</thead>", "<tbody>"]
|
||||
)
|
||||
for row in rows:
|
||||
parts.append("<tr>")
|
||||
for j in range(len(headers)):
|
||||
parts.append(f"<td{aligns[j] if j < len(aligns) else ''}>{render_inline(row[j] if j < len(row) else '', page)}</td>")
|
||||
parts.append("</tr>")
|
||||
out.append("\n".join(parts + ["</tbody>", "</table>"]))
|
||||
continue
|
||||
|
||||
if _list_info(line):
|
||||
items: list[tuple[int, str, list[str]]] = []
|
||||
while i < n:
|
||||
if not lines[i].strip():
|
||||
if i + 1 < n and _list_info(lines[i + 1]):
|
||||
i += 1
|
||||
continue
|
||||
break
|
||||
info = _list_info(lines[i])
|
||||
if not info:
|
||||
break
|
||||
level, kind, body = info
|
||||
chunk = [body]
|
||||
i += 1
|
||||
while i < n and lines[i].strip() and _list_info(lines[i]) is None:
|
||||
t = lines[i].strip()
|
||||
if t.startswith(("```", ">")) or _HEADING.match(t) or _HR.fullmatch(t):
|
||||
break
|
||||
chunk.append(lines[i])
|
||||
i += 1
|
||||
items.append((level, kind, chunk))
|
||||
|
||||
def render_list(items: list[tuple[int, str, list[str]]], start: int, min_level: int) -> tuple[str, int]:
|
||||
if start >= len(items) or items[start][0] < min_level:
|
||||
return "", start
|
||||
kind, chunks, idx = items[start][1], [f"<{items[start][1]}>"], start
|
||||
while idx < len(items) and items[idx][0] >= min_level:
|
||||
level, ikind, body_lines = items[idx]
|
||||
if level > min_level:
|
||||
nested, idx = render_list(items, idx, level)
|
||||
chunks[-1] = (chunks[-1][:-5] + nested + "</li>") if chunks[-1].endswith("</li>") else chunks[-1] + nested
|
||||
continue
|
||||
if ikind != kind:
|
||||
chunks += [f"</{kind}>", f"<{ikind}>"]
|
||||
kind = ikind
|
||||
idx += 1
|
||||
body = render_inline("\n".join(body_lines), page)
|
||||
nested = ""
|
||||
if idx < len(items) and items[idx][0] > min_level:
|
||||
nested, idx = render_list(items, idx, min_level + 1)
|
||||
chunks.append(f"<li>{body}{nested}\n</li>" if nested else f"<li>{body}</li>")
|
||||
chunks.append(f"</{kind}>")
|
||||
return "\n".join(chunks), idx
|
||||
|
||||
out.append(render_list(items, 0, items[0][0])[0])
|
||||
continue
|
||||
|
||||
if s.startswith(">"):
|
||||
q = []
|
||||
while i < n and lines[i].strip().startswith(">"):
|
||||
q.append(re.sub(r"^>\s?", "", lines[i].strip()))
|
||||
i += 1
|
||||
m = _ADMONITION.match(q[0].strip()) if q else None
|
||||
if m:
|
||||
kind = m.group(1).lower()
|
||||
title = m.group(1).capitalize()
|
||||
body = _render_blocks("\n".join(q[1:]), page)
|
||||
out.append(f'<div class="admonition {kind}">\n<p class="admonition-title">{title}</p>\n{body}\n</div>')
|
||||
else:
|
||||
out.append(f"<blockquote>\n<p>{render_inline(chr(10).join(q), page)}</p>\n</blockquote>")
|
||||
continue
|
||||
|
||||
if s.startswith("<!--"):
|
||||
out.append(rewrite_html_urls(line, page))
|
||||
i += 1
|
||||
# Preserve a blank line after HTML comments (python-markdown does).
|
||||
if i < n and not lines[i].strip():
|
||||
out.append("")
|
||||
while i < n and not lines[i].strip():
|
||||
i += 1
|
||||
continue
|
||||
|
||||
buf = [line]
|
||||
i += 1
|
||||
while i < n and lines[i].strip():
|
||||
t = lines[i].strip()
|
||||
if t.startswith(("```", ">")) or _HEADING.match(t) or _HR.fullmatch(t):
|
||||
break
|
||||
if "|" in lines[i] and i + 1 < n and _is_sep(lines[i + 1]):
|
||||
break
|
||||
buf.append(lines[i])
|
||||
i += 1
|
||||
out.append(f"<p>{render_inline(chr(10).join(buf), page)}</p>")
|
||||
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def render_markdown(text: str, page: str) -> str:
|
||||
return inject_glossary(_render_blocks(text, page), page)
|
||||
|
||||
|
||||
def page_title(source: str) -> str:
|
||||
for line in source.splitlines():
|
||||
if line.startswith("# "):
|
||||
return line[2:].strip()
|
||||
return "openpilot docs"
|
||||
|
||||
|
||||
def write_html_redirect(rel: Path) -> None:
|
||||
if rel.name == "index.md":
|
||||
return
|
||||
target = f"{rel.stem}/"
|
||||
out = SITE_DIR / rel.with_suffix(".html")
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"<!doctype html>",
|
||||
f'<meta http-equiv="refresh" content="0; url={html.escape(target)}">',
|
||||
f'<link rel="canonical" href="{html.escape(target)}">',
|
||||
f"<script>location.replace({json.dumps(target)} + location.search + location.hash)</script>",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def copy_assets() -> None:
|
||||
for src in DOCS_DIR.rglob("*"):
|
||||
if not src.is_file():
|
||||
continue
|
||||
rel = src.relative_to(DOCS_DIR)
|
||||
if any(part in EXCLUDE_DIRS for part in rel.parts):
|
||||
continue
|
||||
if src.suffix == ".md" or src in (Path(__file__).resolve(), TEMPLATE_FILE):
|
||||
continue
|
||||
dest = SITE_DIR / rel
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dest)
|
||||
|
||||
|
||||
def render_nav_html(current_page: str) -> str:
|
||||
parts: list[str] = []
|
||||
for title, target in NAV:
|
||||
if target is None:
|
||||
parts.append(f'<div class="nav-section">{html.escape(title)}</div>')
|
||||
elif target.startswith(("http://", "https://")):
|
||||
parts.append(f'<a href="{html.escape(target)}">{html.escape(title)}</a>')
|
||||
else:
|
||||
active = ' class="active"' if target == current_page else ""
|
||||
parts.append(f'<a href="{html.escape(page_href(current_page, target))}"{active}>{html.escape(title)}</a>')
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def build() -> None:
|
||||
template = TEMPLATE_FILE.read_text()
|
||||
pages = [
|
||||
(path.relative_to(DOCS_DIR), path.read_text())
|
||||
for path in sorted(DOCS_DIR.rglob("*.md"))
|
||||
if path != DOCS_DIR / "README.md" and not any(part in EXCLUDE_DIRS for part in path.relative_to(DOCS_DIR).parts)
|
||||
]
|
||||
pages.append((Path(GLOSSARY_PAGE), f"# openpilot glossary\n\n{GLOSSARY_DEFINITIONS}"))
|
||||
pages.sort()
|
||||
|
||||
if SITE_DIR.exists():
|
||||
shutil.rmtree(SITE_DIR)
|
||||
SITE_DIR.mkdir(parents=True)
|
||||
copy_assets()
|
||||
|
||||
for rel_path, source in pages:
|
||||
rel = rel_path.as_posix()
|
||||
body = render_markdown(source, rel)
|
||||
title = page_title(source)
|
||||
route = page_route(rel)
|
||||
root = "../" * (0 if route == "." else len(route.split("/")))
|
||||
edit_path = "serve.py" if rel == GLOSSARY_PAGE else rel
|
||||
page_html = template
|
||||
for name, value in {
|
||||
"TITLE": html.escape(title),
|
||||
"ROOT": root,
|
||||
"HOME_HREF": page_href(rel, "index.md"),
|
||||
"NAV": render_nav_html(rel),
|
||||
"BODY": body,
|
||||
"EDIT_URL": html.escape(f"{REPO_URL}blob/master/docs/{edit_path}"),
|
||||
}.items():
|
||||
page_html = page_html.replace(f"{{{{{name}}}}}", value)
|
||||
out = SITE_DIR / ("" if route == "." else route) / "index.html"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(page_html)
|
||||
write_html_redirect(rel_path)
|
||||
|
||||
print(f"docs: built {len(pages)} pages into {SITE_DIR}")
|
||||
|
||||
|
||||
def _watched_files() -> list[Path]:
|
||||
return [p for p in DOCS_DIR.rglob("*") if p.is_file() and not any(part in EXCLUDE_DIRS for part in p.relative_to(DOCS_DIR).parts)]
|
||||
|
||||
|
||||
def serve() -> None:
|
||||
build()
|
||||
mtimes = {p: p.stat().st_mtime for p in _watched_files()}
|
||||
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(SITE_DIR))
|
||||
httpd = http.server.ThreadingHTTPServer(("", 0), handler)
|
||||
print(f"docs: serving on http://localhost:{httpd.server_port}/ (watching for changes)")
|
||||
try:
|
||||
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
||||
while True:
|
||||
time.sleep(0.5)
|
||||
new_mtimes = {p: p.stat().st_mtime for p in _watched_files()}
|
||||
if new_mtimes != mtimes:
|
||||
mtimes = new_mtimes
|
||||
print("docs: change detected, rebuilding...")
|
||||
try:
|
||||
build()
|
||||
except Exception as e:
|
||||
print(f"docs: build failed: {e}")
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Build or serve the openpilot documentation site.")
|
||||
parser.add_argument("--build", action="store_true", help="Build the site and exit.")
|
||||
args = parser.parse_args()
|
||||
if args.build:
|
||||
build()
|
||||
else:
|
||||
serve()
|
||||
@@ -1,42 +0,0 @@
|
||||
.md-logo img {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.glossary-term {
|
||||
position: relative;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.glossary-term__label {
|
||||
border-bottom: 1px dotted currentColor;
|
||||
}
|
||||
|
||||
.glossary-term__tooltip {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.4rem);
|
||||
left: 50%;
|
||||
width: max-content;
|
||||
max-width: min(30rem, 80vw);
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: 0.6rem;
|
||||
background: rgb(26 26 26 / 96%);
|
||||
color: white;
|
||||
box-shadow: 0 0.6rem 1.8rem rgb(0 0 0 / 22%);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%) translateY(-0.15rem);
|
||||
transition: opacity 120ms ease, transform 120ms ease;
|
||||
visibility: hidden;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.glossary-term:hover .glossary-term__tooltip,
|
||||
.glossary-term:focus-visible .glossary-term__tooltip,
|
||||
.glossary-term:focus-within .glossary-term__tooltip {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{TITLE}} · openpilot docs</title>
|
||||
<link rel="icon" href="{{ROOT}}assets/favicon.svg">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fff;
|
||||
--bg-elev: #f5f5f5;
|
||||
--bg-hover: #eef0ff;
|
||||
--fg: #262626;
|
||||
--fg-dim: #666;
|
||||
--accent: #4051b5;
|
||||
--border: #e5e5e5;
|
||||
--max-width: 76rem;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { scrollbar-gutter: stable; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
header.site {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
min-height: 3rem;
|
||||
padding: 0.5rem max(1.25rem, calc((100% - var(--max-width)) / 2));
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
}
|
||||
header.site img { display: block; height: 24px; filter: invert(1); }
|
||||
header.site .brand { display: flex; align-items: center; gap: 0.75rem; color: var(--fg); }
|
||||
header.site .brand:hover { text-decoration: none; }
|
||||
header.site .site-name { font-weight: 600; font-size: 1.05rem; }
|
||||
header.site .spacer { flex: 1; }
|
||||
header.site .social { display: flex; gap: 1rem; }
|
||||
header.site .social a { display: flex; color: var(--fg); }
|
||||
header.site .social a:hover { color: var(--accent); }
|
||||
header.site .social svg { width: 1.75rem; height: 1.75rem; fill: currentColor; }
|
||||
header.site .social a[aria-label="Discord"] svg { transform: scale(1.15); }
|
||||
|
||||
.layout { display: flex; width: 100%; max-width: var(--max-width); margin: 0 auto; }
|
||||
|
||||
nav.sidebar {
|
||||
width: 14rem; flex-shrink: 0;
|
||||
padding: 3.3rem 1.25rem 2rem 0;
|
||||
position: sticky; top: 3rem; align-self: start;
|
||||
max-height: calc(100vh - 3rem); overflow-y: auto;
|
||||
}
|
||||
nav.sidebar .nav-section { font-weight: 600; margin: 1.15rem 0.8rem 0.45rem; }
|
||||
nav.sidebar a {
|
||||
display: block; padding: 0.28rem 0.8rem; border-radius: 0.45rem;
|
||||
color: var(--fg); line-height: 1.35;
|
||||
}
|
||||
nav.sidebar a:hover { background: var(--bg-hover); text-decoration: none; }
|
||||
nav.sidebar a.active { background: var(--bg-hover); color: var(--accent); font-weight: 600; }
|
||||
|
||||
main.content { width: min(100%, 46.5rem); min-width: 0; padding: 2.7rem 2rem 5rem; }
|
||||
main.content h1, main.content h2, main.content h3 { line-height: 1.25; margin-top: 1.8rem; }
|
||||
main.content h1 { margin-top: 0; }
|
||||
main.content img { max-width: 100%; }
|
||||
main.content table { border-collapse: collapse; display: block; overflow-x: auto; }
|
||||
main.content th, main.content td { border: 1px solid var(--border); padding: 0.4rem 0.6rem; text-align: left; }
|
||||
main.content th { background: var(--bg-elev); }
|
||||
main.content pre {
|
||||
position: relative;
|
||||
background: var(--bg-elev);
|
||||
padding: 0.9rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
main.content code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
main.content :not(pre) > code { background: var(--bg-elev); padding: 0.1rem 0.35rem; border-radius: 0.2rem; font-size: 0.88em; }
|
||||
main.content blockquote { border-left: 3px solid var(--border); margin: 1rem 0; padding: 0.2rem 1rem; color: var(--fg-dim); }
|
||||
main.content .admonition {
|
||||
margin: 1rem 0; padding: 0.5rem 1rem; border-radius: 0.3rem; border-left: 3px solid var(--border);
|
||||
background: var(--bg-elev);
|
||||
}
|
||||
main.content .admonition .admonition-title { margin: 0.2rem 0 0.4rem; font-weight: 600; }
|
||||
main.content .admonition.note { border-left-color: #0969da; }
|
||||
main.content .admonition.note .admonition-title { color: #0969da; }
|
||||
main.content .admonition.tip { border-left-color: #1a7f37; }
|
||||
main.content .admonition.tip .admonition-title { color: #1a7f37; }
|
||||
main.content .admonition.important { border-left-color: #8250df; }
|
||||
main.content .admonition.important .admonition-title { color: #8250df; }
|
||||
main.content .admonition.warning { border-left-color: #9a6700; }
|
||||
main.content .admonition.warning .admonition-title { color: #9a6700; }
|
||||
main.content .admonition.caution { border-left-color: #cf222e; }
|
||||
main.content .admonition.caution .admonition-title { color: #cf222e; }
|
||||
main.content details { margin: 0.5rem 0; }
|
||||
main.content hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
||||
.headerlink { margin-left: 0.25rem; opacity: 0; font-size: 0.7em; }
|
||||
h1:hover .headerlink, h2:hover .headerlink, h3:hover .headerlink { opacity: 1; }
|
||||
|
||||
.edit-link { margin-top: 3rem; font-size: 0.85rem; }
|
||||
|
||||
.copy-btn {
|
||||
position: absolute; top: 0.4rem; right: 0.4rem;
|
||||
background: var(--bg); color: var(--fg-dim);
|
||||
border: 1px solid var(--border); border-radius: 0.35rem;
|
||||
padding: 0.2rem 0.5rem; font-size: 0.78rem; cursor: pointer;
|
||||
opacity: 0; transition: opacity 120ms;
|
||||
}
|
||||
main.content pre:hover .copy-btn { opacity: 1; }
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
nav.sidebar { display: none; }
|
||||
main.content { padding: 2rem 1.25rem 4rem; }
|
||||
}
|
||||
|
||||
.glossary-term {
|
||||
position: relative;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.glossary-term__label {
|
||||
border-bottom: 1px dotted currentColor;
|
||||
}
|
||||
|
||||
.glossary-term__tooltip {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.4rem);
|
||||
left: 50%;
|
||||
width: max-content;
|
||||
max-width: min(30rem, 80vw);
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: 0.6rem;
|
||||
background: rgb(26 26 26 / 96%);
|
||||
color: white;
|
||||
box-shadow: 0 0.6rem 1.8rem rgb(0 0 0 / 22%);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%) translateY(-0.15rem);
|
||||
transition: opacity 120ms ease, transform 120ms ease;
|
||||
visibility: hidden;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.glossary-term:hover .glossary-term__tooltip,
|
||||
.glossary-term:focus-visible .glossary-term__tooltip,
|
||||
.glossary-term:focus-within .glossary-term__tooltip {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site">
|
||||
<a class="brand" href="{{HOME_HREF}}">
|
||||
<img src="{{ROOT}}assets/comma-logo.png" alt="">
|
||||
<span class="site-name">openpilot docs</span>
|
||||
</a>
|
||||
<span class="spacer"></span>
|
||||
<div class="social">
|
||||
<a href="https://github.com/commaai" aria-label="GitHub">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
||||
M12 .7A11.5 11.5 0 0 0 8.4 23c.6.1.8-.2.8-.5v-2c-3.3.7-4-1.4-4-1.4-.5-1.4-1.3-1.8-1.3-1.8-1.1-.7.1-.7.1-.7
|
||||
1.2.1 1.8 1.2 1.8 1.2 1.1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.7-.3-5.5-1.3-5.5-5.7 0-1.3.5-2.3 1.2-3.1
|
||||
-.1-.3-.5-1.6.1-3.1 0 0 1-.3 3.2 1.2a11 11 0 0 1 5.8 0C15.8 6 16.8 6.3 16.8 6.3c.6 1.5.2 2.8.1 3.1
|
||||
.8.8 1.2 1.8 1.2 3.1 0 4.4-2.8 5.4-5.5 5.7.4.4.8 1.1.8 2.2v2.1c0 .3.2.6.8.5A11.5 11.5 0 0 0 12 .7Z"/></svg>
|
||||
</a>
|
||||
<a href="https://discord.comma.ai" aria-label="Discord">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
||||
M20.3 4.4A16 16 0 0 0 16.3 3l-.5 1.1a15 15 0 0 0-7.6 0L7.7 3a16 16 0 0 0-4 1.4C1.1 8.2.4 11.9.8 15.5
|
||||
a16 16 0 0 0 4.9 2.6l1.2-1.7-1.8-.9.4-.3c3.5 1.6 9.4 1.6 13 0l.4.3-1.8.9 1.2 1.7a16 16 0 0 0 4.9-2.6
|
||||
c.5-4.2-.8-7.8-2.9-11.1ZM8.3 13.3c-1.1 0-1.9-1-1.9-2.2 0-1.3.8-2.3 1.9-2.3s2 1 1.9 2.3c0 1.2-.8 2.2-1.9 2.2Z
|
||||
m7.4 0c-1.1 0-1.9-1-1.9-2.2 0-1.3.8-2.3 1.9-2.3s2 1 1.9 2.3c0 1.2-.8 2.2-1.9 2.2Z"/></svg>
|
||||
</a>
|
||||
<a href="https://x.com/comma_ai" aria-label="X">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
||||
M18.2 2.3h3.3l-7.2 8.3 8.5 11.2h-6.7l-5.2-6.8-6 6.8H1.6l7.8-8.9L1.2 2.3h6.8l4.7 6.2 5.5-6.2Z
|
||||
m-1.2 17.5h1.8L7 4.2H5Z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<nav class="sidebar">
|
||||
{{NAV}}
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
{{BODY}}
|
||||
<div class="edit-link">
|
||||
<a href="{{EDIT_URL}}">Edit this page on GitHub</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('pre').forEach(function (el) {
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'copy-btn';
|
||||
btn.textContent = 'copy';
|
||||
btn.addEventListener('click', function () {
|
||||
var code = el.querySelector('code');
|
||||
if (!code) return;
|
||||
navigator.clipboard.writeText(code.innerText).then(function () {
|
||||
btn.textContent = 'copied';
|
||||
setTimeout(function () { btn.textContent = 'copy'; }, 1200);
|
||||
});
|
||||
});
|
||||
el.appendChild(btn);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+3
-3
@@ -19,12 +19,12 @@ function agnos_init {
|
||||
|
||||
# Check if AGNOS update is required
|
||||
if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then
|
||||
AGNOS_PY="$DIR/openpilot/common/hardware/tici/agnos.py"
|
||||
MANIFEST="$DIR/openpilot/system/hardware/tici/agnos.json"
|
||||
AGNOS_PY="$DIR/openpilot/common/hardware/comma/agnos.py"
|
||||
MANIFEST="$DIR/openpilot/system/hardware/comma/agnos.json"
|
||||
if $AGNOS_PY --verify $MANIFEST; then
|
||||
sudo reboot
|
||||
fi
|
||||
$DIR/openpilot/common/hardware/tici/updater $AGNOS_PY $MANIFEST
|
||||
$DIR/openpilot/common/hardware/comma/updater $AGNOS_PY $MANIFEST
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
|
||||
export QCOM_PRIORITY=12
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="18.5"
|
||||
export AGNOS_VERSION="19.6"
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
+1
-1
Submodule msgq_repo updated: bb6cc57ef4...e7396e76da
+1
-1
Submodule opendbc_repo updated: ae445c9b5e...3b8f263e96
@@ -775,3 +775,13 @@ struct GpsTrajectory @0x8cfeb072f5301000 {
|
||||
x @0 :List(Float32);
|
||||
y @1 :List(Float32);
|
||||
}
|
||||
|
||||
struct AudioFeedbackDEPRECATED @0xed47e3c075be372a {
|
||||
audio @0 :AudioData;
|
||||
blockNum @1 :UInt16;
|
||||
|
||||
struct AudioData {
|
||||
data @0 :Data;
|
||||
sampleRate @1 :UInt32;
|
||||
}
|
||||
}
|
||||
|
||||
+59
-38
@@ -75,7 +75,6 @@ struct OnroadEvent @0xc4fa6047f024e718 {
|
||||
driverUnresponsive2 @37;
|
||||
driverUnresponsive3 @38;
|
||||
belowSteerSpeed @39;
|
||||
lowBattery @40;
|
||||
accFaulted @41;
|
||||
sensorDataInvalid @42;
|
||||
commIssue @43;
|
||||
@@ -107,18 +106,17 @@ struct OnroadEvent @0xc4fa6047f024e718 {
|
||||
noGps @68;
|
||||
wrongCruiseMode @69;
|
||||
modeldLagging @70;
|
||||
deviceFalling @71;
|
||||
fanMalfunction @72;
|
||||
cameraMalfunction @73;
|
||||
cameraFrameRate @74;
|
||||
processNotRunning @75;
|
||||
dashcamMode @76;
|
||||
selfdriveInitializing @77;
|
||||
usbError @78;
|
||||
cruiseMismatch @79;
|
||||
canBusMissing @80;
|
||||
selfdrivedLagging @81;
|
||||
resumeBlocked @82;
|
||||
carNotReady @103;
|
||||
steerTimeLimit @83;
|
||||
vehicleSensorsInvalid @84;
|
||||
locationdTemporaryError @85;
|
||||
@@ -131,9 +129,15 @@ struct OnroadEvent @0xc4fa6047f024e718 {
|
||||
aeb @92;
|
||||
userBookmark @95;
|
||||
excessiveActuation @96;
|
||||
audioFeedback @97;
|
||||
bigModelLoading @100;
|
||||
bigModelFailed @102;
|
||||
|
||||
lowBatteryDEPRECATED @40;
|
||||
soundsUnavailableDEPRECATED @47;
|
||||
deviceFallingDEPRECATED @71;
|
||||
usbErrorDEPRECATED @78;
|
||||
audioFeedbackDEPRECATED @97;
|
||||
bigModelReadyDEPRECATED @101;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,13 +180,13 @@ struct InitData {
|
||||
|
||||
enum DeviceType {
|
||||
unknown @0;
|
||||
neo @1;
|
||||
neo @1; # NEO, EON, & comma two
|
||||
chffrAndroid @2;
|
||||
chffrIos @3;
|
||||
tici @4;
|
||||
tici @4; # comma three
|
||||
pc @5;
|
||||
tizi @6;
|
||||
mici @7;
|
||||
tizi @6; # comma 3X
|
||||
mici @7; # comma four
|
||||
}
|
||||
|
||||
struct PandaInfo {
|
||||
@@ -700,9 +704,29 @@ struct UsbState {
|
||||
manufacturer @6 :Text;
|
||||
product @5 :Text;
|
||||
linkErrorCount @7 :UInt16;
|
||||
usb3Lane @8 :Usb3Lane;
|
||||
|
||||
enum Usb3Lane {
|
||||
unknown @0;
|
||||
a @1;
|
||||
b @2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChestnutState {
|
||||
tempC @0 :Float32;
|
||||
memoryTempC @1 :Float32;
|
||||
powerDrawW @2 :Float32;
|
||||
powerLimitW @3 :Float32;
|
||||
gpuUsagePercent @4 :UInt8;
|
||||
gpuClockMhz @5 :UInt16;
|
||||
fanSpeedRpm @6 :UInt16;
|
||||
pcieLtssm @7 :UInt8;
|
||||
supplyVoltage @8 :UInt16; # mV
|
||||
supplyCurrent @9 :Int16; # mA
|
||||
}
|
||||
|
||||
struct RadarState @0x9a185389d6fdd05f {
|
||||
mdMonoTime @6 :UInt64; # for debugging
|
||||
radarErrors @13 :Car.RadarData.Error;
|
||||
@@ -746,7 +770,7 @@ struct RadarState @0x9a185389d6fdd05f {
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveCalibrationData {
|
||||
struct ExtrinsicsCalibration @0x96df70754d8390bc {
|
||||
calStatus @11 :Status;
|
||||
calCycle @2 :Int32;
|
||||
calPerc @3 :Int8;
|
||||
@@ -1026,6 +1050,7 @@ struct ModelDataV2 {
|
||||
timestampEof @3 :UInt64;
|
||||
modelExecutionTime @15 :Float32;
|
||||
rawPredictions @16 :Data;
|
||||
big @27 :Bool;
|
||||
|
||||
# predicted future position, orientation, etc..
|
||||
position @4 :XYZTData;
|
||||
@@ -1366,7 +1391,7 @@ struct LiveLocationKalman {
|
||||
}
|
||||
|
||||
|
||||
struct LivePose {
|
||||
struct DeviceMotion @0xc24ca2b57206b44d {
|
||||
# More info on reference frames:
|
||||
# https://github.com/commaai/openpilot/tree/master/openpilot/common/transformations
|
||||
orientationNED @0 :XYZMeasurement;
|
||||
@@ -2248,7 +2273,7 @@ struct Boot {
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveParametersData {
|
||||
struct VehicleParameters @0xd9058dcb967c2753 {
|
||||
valid @0 :Bool;
|
||||
gyroBias @1 :Float32;
|
||||
angleOffsetDeg @2 :Float32;
|
||||
@@ -2282,8 +2307,8 @@ struct LiveParametersData {
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveTorqueParametersData {
|
||||
liveValid @0 :Bool;
|
||||
struct LateralTorqueParameters @0xe61690eb0b091692 {
|
||||
valid @0 :Bool;
|
||||
latAccelFactorRaw @1 :Float32;
|
||||
latAccelOffsetRaw @2 :Float32;
|
||||
frictionCoefficientRaw @3 :Float32;
|
||||
@@ -2299,7 +2324,7 @@ struct LiveTorqueParametersData {
|
||||
calPerc @13 :Int8;
|
||||
}
|
||||
|
||||
struct LiveDelayData {
|
||||
struct LateralDelay @0x98dfdb22c44df8d4 {
|
||||
lateralDelay @0 :Float32;
|
||||
validBlocks @1 :Int32;
|
||||
status @2 :Status;
|
||||
@@ -2485,11 +2510,6 @@ struct AudioData {
|
||||
sampleRate @1 :UInt32;
|
||||
}
|
||||
|
||||
struct AudioFeedback {
|
||||
audio @0 :AudioData;
|
||||
blockNum @1 :UInt16;
|
||||
}
|
||||
|
||||
struct Touch {
|
||||
sec @0 :Int64;
|
||||
usec @1 :Int64;
|
||||
@@ -2522,9 +2542,9 @@ struct Event {
|
||||
pandaStates @81 :List(PandaState);
|
||||
peripheralState @80 :PeripheralState;
|
||||
radarState @13 :RadarState;
|
||||
liveTracks @131 :Car.RadarData;
|
||||
radarTracks @131 :Car.RadarData;
|
||||
sendcan @17 :List(CanData);
|
||||
liveCalibration @19 :LiveCalibrationData;
|
||||
extrinsicsCalibration @19 :ExtrinsicsCalibration;
|
||||
carState @22 :Car.CarState;
|
||||
carControl @23 :Car.CarControl;
|
||||
carOutput @127 :Car.CarOutput;
|
||||
@@ -2535,31 +2555,31 @@ struct Event {
|
||||
qcomGnss @31 :QcomGnss;
|
||||
gpsLocationExternal @48 :GpsLocationData;
|
||||
gpsLocation @21 :GpsLocationData;
|
||||
liveParameters @61 :LiveParametersData;
|
||||
liveTorqueParameters @94 :LiveTorqueParametersData;
|
||||
liveDelay @146 : LiveDelayData;
|
||||
vehicleParameters @61 :VehicleParameters;
|
||||
lateralTorqueParameters @94 :LateralTorqueParameters;
|
||||
lateralDelay @146 : LateralDelay;
|
||||
cameraOdometry @63 :CameraOdometry;
|
||||
thumbnail @66: Thumbnail;
|
||||
onroadEvents @134: List(OnroadEvent);
|
||||
carParams @69: Car.CarParams;
|
||||
driverMonitoringState @151 :DriverMonitoringState;
|
||||
livePose @129 :LivePose;
|
||||
deviceMotion @129 :DeviceMotion;
|
||||
modelV2 @75 :ModelDataV2;
|
||||
drivingModelData @128 :DrivingModelData;
|
||||
driverStateV2 @92 :DriverStateV2;
|
||||
|
||||
# camera stuff, each camera state has a matching encode idx
|
||||
roadCameraState @2 :FrameData;
|
||||
driverCameraState @70: FrameData;
|
||||
narrowRoadCameraState @2 :FrameData;
|
||||
cabinCameraState @70: FrameData;
|
||||
wideRoadCameraState @74: FrameData;
|
||||
roadEncodeIdx @15 :EncodeIndex;
|
||||
driverEncodeIdx @76 :EncodeIndex;
|
||||
narrowRoadEncodeIdx @15 :EncodeIndex;
|
||||
cabinEncodeIdx @76 :EncodeIndex;
|
||||
wideRoadEncodeIdx @77 :EncodeIndex;
|
||||
qRoadEncodeIdx @90 :EncodeIndex;
|
||||
qNarrowRoadEncodeIdx @90 :EncodeIndex;
|
||||
|
||||
livestreamRoadEncodeIdx @117 :EncodeIndex;
|
||||
livestreamNarrowRoadEncodeIdx @117 :EncodeIndex;
|
||||
livestreamWideRoadEncodeIdx @118 :EncodeIndex;
|
||||
livestreamDriverEncodeIdx @119 :EncodeIndex;
|
||||
livestreamCabinEncodeIdx @119 :EncodeIndex;
|
||||
|
||||
# microphone data
|
||||
soundPressure @103 :SoundPressure;
|
||||
@@ -2571,6 +2591,7 @@ struct Event {
|
||||
procLog @33 :ProcLog;
|
||||
clocks @35 :Clocks;
|
||||
deviceState @6 :DeviceState;
|
||||
chestnutState @152 :ChestnutState;
|
||||
logMessage @18 :Text;
|
||||
errorLogMessage @85 :Text;
|
||||
|
||||
@@ -2583,21 +2604,20 @@ struct Event {
|
||||
# driving feedback
|
||||
userBookmark @93 :UserBookmark;
|
||||
bookmarkButton @148 :UserBookmark;
|
||||
audioFeedback @149 :AudioFeedback;
|
||||
|
||||
lateralManeuverPlan @150 :LateralManeuverPlan;
|
||||
|
||||
# *********** debug ***********
|
||||
testJoystick @52 :Joystick;
|
||||
roadEncodeData @86 :EncodeData;
|
||||
driverEncodeData @87 :EncodeData;
|
||||
narrowRoadEncodeData @86 :EncodeData;
|
||||
cabinEncodeData @87 :EncodeData;
|
||||
wideRoadEncodeData @88 :EncodeData;
|
||||
qRoadEncodeData @89 :EncodeData;
|
||||
qNarrowRoadEncodeData @89 :EncodeData;
|
||||
alertDebug @133 :DebugAlert;
|
||||
|
||||
livestreamRoadEncodeData @120 :EncodeData;
|
||||
livestreamNarrowRoadEncodeData @120 :EncodeData;
|
||||
livestreamWideRoadEncodeData @121 :EncodeData;
|
||||
livestreamDriverEncodeData @122 :EncodeData;
|
||||
livestreamCabinEncodeData @122 :EncodeData;
|
||||
|
||||
# *********** Custom: reserved for forks ***********
|
||||
|
||||
@@ -2633,6 +2653,7 @@ struct Event {
|
||||
|
||||
# *********** legacy + deprecated ***********
|
||||
model @9 :Deprecated.ModelData; # TODO: rename modelV2 and mark this as deprecated
|
||||
audioFeedbackDEPRECATED @149 :Deprecated.AudioFeedbackDEPRECATED;
|
||||
liveMpcDEPRECATED @36 :Deprecated.LiveMpcData;
|
||||
liveLongitudinalMpcDEPRECATED @37 :Deprecated.LiveLongitudinalMpcData;
|
||||
liveLocationKalmanDeprecatedDEPRECATED @51 :Deprecated.LiveLocationData;
|
||||
|
||||
@@ -4,6 +4,7 @@ import numbers
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.parameterized import parameterized
|
||||
|
||||
from openpilot.cereal import log
|
||||
@@ -46,7 +47,7 @@ def delayed_send(delay, sock, dat):
|
||||
threading.Timer(delay, send_func).start()
|
||||
|
||||
|
||||
class TestMessaging:
|
||||
class TestMessaging(OpenpilotTestCase):
|
||||
@parameterized.expand(events)
|
||||
def test_new_message(self, evt):
|
||||
try:
|
||||
@@ -80,10 +81,11 @@ class TestMessaging:
|
||||
assert len(msgs) == 0
|
||||
|
||||
# no wait but msgs are queued up
|
||||
pub_sock.send(messaging.new_message(sock).to_bytes())
|
||||
assert sub_sock.receive() is not None # synchronize the PUB/SUB connection
|
||||
num_msgs = random.randrange(3, 10)
|
||||
for _ in range(num_msgs):
|
||||
pub_sock.send(messaging.new_message(sock).to_bytes())
|
||||
time.sleep(0.1)
|
||||
msgs = func(sub_sock)
|
||||
assert isinstance(msgs, list)
|
||||
assert all(isinstance(msg, expected_type) for msg in msgs)
|
||||
@@ -110,7 +112,7 @@ class TestMessaging:
|
||||
def test_recv_one(self):
|
||||
sock = "carState"
|
||||
pub_sock = messaging.pub_sock(sock)
|
||||
sub_sock = messaging.sub_sock(sock, timeout=1000)
|
||||
sub_sock = messaging.sub_sock(sock, timeout=10)
|
||||
|
||||
# no msg in queue, socket should timeout
|
||||
recvd = messaging.recv_one(sub_sock)
|
||||
@@ -141,7 +143,7 @@ class TestMessaging:
|
||||
|
||||
def test_recv_one_retry(self):
|
||||
sock = "carState"
|
||||
sock_timeout = 0.1
|
||||
sock_timeout = 0.005
|
||||
pub_sock = messaging.pub_sock(sock)
|
||||
sub_sock = messaging.sub_sock(sock, timeout=round(sock_timeout*1000))
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ import time
|
||||
from typing import cast
|
||||
from collections.abc import Sized
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.cereal.messaging.tests.test_messaging import events, random_sock, random_socks, \
|
||||
random_bytes, random_carstate, assert_carstate
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
|
||||
|
||||
class TestSubMaster:
|
||||
class TestSubMaster(OpenpilotTestCase):
|
||||
|
||||
def test_init(self):
|
||||
sm = messaging.SubMaster(events)
|
||||
@@ -59,17 +60,17 @@ class TestSubMaster:
|
||||
def test_update_timeout(self):
|
||||
sock = random_sock()
|
||||
sm = messaging.SubMaster([sock,])
|
||||
timeout = random.randrange(1000, 3000)
|
||||
timeout = random.randrange(10, 30)
|
||||
start_time = time.monotonic()
|
||||
sm.update(timeout)
|
||||
t = time.monotonic() - start_time
|
||||
assert t >= timeout/1000.
|
||||
assert t < 3
|
||||
assert t < 0.1
|
||||
assert not any(sm.updated.values())
|
||||
|
||||
def test_avg_frequency_checks(self):
|
||||
for poll in (True, False):
|
||||
sm = messaging.SubMaster(["modelV2", "carParams", "carState", "cameraOdometry", "liveCalibration"],
|
||||
sm = messaging.SubMaster(["modelV2", "carParams", "carState", "cameraOdometry", "extrinsicsCalibration"],
|
||||
poll=("modelV2" if poll else None),
|
||||
frequency=(20. if not poll else None))
|
||||
|
||||
@@ -77,7 +78,7 @@ class TestSubMaster:
|
||||
"carState": (20, 20),
|
||||
"modelV2": (20, 20 if poll else 10),
|
||||
"cameraOdometry": (20, 10),
|
||||
"liveCalibration": (4, 4),
|
||||
"extrinsicsCalibration": (4, 4),
|
||||
"carParams": (None, None),
|
||||
"userBookmark": (None, None),
|
||||
}
|
||||
@@ -97,17 +98,19 @@ class TestSubMaster:
|
||||
pub_sock = messaging.pub_sock(sock)
|
||||
sm = messaging.SubMaster([sock,])
|
||||
|
||||
pub_sock.send(messaging.new_message(sock).to_bytes())
|
||||
sm.update(1000) # synchronize the PUB/SUB connection
|
||||
|
||||
n = 10
|
||||
for i in range(n+1):
|
||||
msg = messaging.new_message(sock)
|
||||
msg.carState.vEgo = i
|
||||
pub_sock.send(msg.to_bytes())
|
||||
time.sleep(0.01)
|
||||
sm.update(1000)
|
||||
assert sm[sock].vEgo == n
|
||||
|
||||
|
||||
class TestPubMaster:
|
||||
class TestPubMaster(OpenpilotTestCase):
|
||||
|
||||
def test_init(self):
|
||||
messaging.PubMaster(events)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.parameterized import parameterized
|
||||
|
||||
import openpilot.cereal.services as services
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
|
||||
|
||||
class TestServices:
|
||||
class TestServices(OpenpilotTestCase):
|
||||
|
||||
@parameterized.expand(SERVICE_LIST.keys())
|
||||
def test_services(self, s):
|
||||
|
||||
@@ -25,6 +25,7 @@ _services: dict[str, tuple] = {
|
||||
"accelerometer": (True, 104., 104),
|
||||
"temperatureSensor": (True, 2., 200),
|
||||
"deviceState": (True, 2., 1),
|
||||
"chestnutState": (True, 10., 10),
|
||||
"touch": (True, 20., 1),
|
||||
"can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment
|
||||
"controlsState": (True, 100., 10, QueueSize.MEDIUM),
|
||||
@@ -32,14 +33,14 @@ _services: dict[str, tuple] = {
|
||||
"pandaStates": (True, 10., 1),
|
||||
"peripheralState": (True, 2., 1),
|
||||
"radarState": (True, 20., 5),
|
||||
"roadEncodeIdx": (False, 20., 1),
|
||||
"liveTracks": (True, 20.),
|
||||
"narrowRoadEncodeIdx": (False, 20., 1),
|
||||
"radarTracks": (True, 20.),
|
||||
"sendcan": (True, 100., 139, QueueSize.MEDIUM),
|
||||
"logMessage": (True, 0., None, QueueSize.BIG),
|
||||
"errorLogMessage": (True, 0., 1, QueueSize.BIG),
|
||||
"liveCalibration": (True, 4., 4),
|
||||
"liveTorqueParameters": (True, 4., 1),
|
||||
"liveDelay": (True, 4., 1),
|
||||
"extrinsicsCalibration": (True, 4., 4),
|
||||
"lateralTorqueParameters": (True, 4., 1),
|
||||
"lateralDelay": (True, 4., 1),
|
||||
"operatingSystemLog": (True, 0.),
|
||||
"carState": (True, 100., 10),
|
||||
"carControl": (True, 100., 10),
|
||||
@@ -54,15 +55,15 @@ _services: dict[str, tuple] = {
|
||||
"qcomGnss": (True, 2.),
|
||||
"clocks": (True, 0.1, 1),
|
||||
"ubloxRaw": (True, 20.),
|
||||
"livePose": (True, 20., 4),
|
||||
"liveParameters": (True, 20., 5),
|
||||
"deviceMotion": (True, 20., 4),
|
||||
"vehicleParameters": (True, 20., 5),
|
||||
"cameraOdometry": (True, 20., 10),
|
||||
"thumbnail": (True, 1 / 60., 1),
|
||||
"onroadEvents": (True, 1., 1),
|
||||
"carParams": (True, 0.02, 1),
|
||||
"roadCameraState": (True, 20., 20),
|
||||
"driverCameraState": (True, 20., 20),
|
||||
"driverEncodeIdx": (False, 20., 1),
|
||||
"narrowRoadCameraState": (True, 20., 20),
|
||||
"cabinCameraState": (True, 20., 20),
|
||||
"cabinEncodeIdx": (False, 20., 1),
|
||||
"driverStateV2": (True, 20., 10),
|
||||
"driverMonitoringState": (True, 20., 10),
|
||||
"wideRoadEncodeIdx": (False, 20., 1),
|
||||
@@ -70,16 +71,15 @@ _services: dict[str, tuple] = {
|
||||
"drivingModelData": (True, 20., 10),
|
||||
"modelV2": (True, 20., None, QueueSize.BIG),
|
||||
"managerState": (True, 2., 1),
|
||||
"qRoadEncodeIdx": (False, 20.),
|
||||
"qNarrowRoadEncodeIdx": (False, 20.),
|
||||
"userBookmark": (True, 0., 1),
|
||||
"soundPressure": (True, 10., 10),
|
||||
"rawAudioData": (False, 20.),
|
||||
"bookmarkButton": (True, 0., 1),
|
||||
"audioFeedback": (True, 0., 1),
|
||||
"roadEncodeData": (False, 20., None, QueueSize.BIG),
|
||||
"driverEncodeData": (False, 20., None, QueueSize.BIG),
|
||||
"narrowRoadEncodeData": (False, 20., None, QueueSize.BIG),
|
||||
"cabinEncodeData": (False, 20., None, QueueSize.BIG),
|
||||
"wideRoadEncodeData": (False, 20., None, QueueSize.BIG),
|
||||
"qRoadEncodeData": (False, 20., None, QueueSize.BIG),
|
||||
"qNarrowRoadEncodeData": (False, 20., None, QueueSize.BIG),
|
||||
|
||||
# sunnypilot
|
||||
"modelManagerSP": (False, 1., 1, QueueSize.BIG),
|
||||
@@ -99,11 +99,11 @@ _services: dict[str, tuple] = {
|
||||
"testJoystick": (True, 0.),
|
||||
"alertDebug": (True, 20., 5),
|
||||
"livestreamWideRoadEncodeIdx": (False, 20.),
|
||||
"livestreamRoadEncodeIdx": (False, 20.),
|
||||
"livestreamDriverEncodeIdx": (False, 20.),
|
||||
"livestreamNarrowRoadEncodeIdx": (False, 20.),
|
||||
"livestreamCabinEncodeIdx": (False, 20.),
|
||||
"livestreamWideRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
||||
"livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
||||
"livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
||||
"livestreamNarrowRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
||||
"livestreamCabinEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
||||
"customReservedRawData0": (True, 0.),
|
||||
}
|
||||
SERVICE_LIST = {name: Service(*vals) for
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class VisionStreamType(IntEnum):
|
||||
VISION_STREAM_NARROW_ROAD = 0
|
||||
VISION_STREAM_CABIN = 1
|
||||
VISION_STREAM_WIDE_ROAD = 2
|
||||
VISION_STREAM_MAP = 3
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "msgq/visionipc/visionbuf.h"
|
||||
|
||||
enum VisionStreamValues : VisionStreamType {
|
||||
VISION_STREAM_NARROW_ROAD = 0,
|
||||
VISION_STREAM_CABIN = 1,
|
||||
VISION_STREAM_WIDE_ROAD = 2,
|
||||
VISION_STREAM_MAP = 3,
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
Import('env', 'envCython')
|
||||
Import('env')
|
||||
|
||||
common_libs = [
|
||||
'params.cc',
|
||||
@@ -11,14 +11,9 @@ common_libs = [
|
||||
_common = env.Library('common', common_libs, LIBS="json11")
|
||||
Export('_common')
|
||||
|
||||
if GetOption('extras'):
|
||||
env.Program('tests/test_common',
|
||||
['tests/test_runner.cc', 'tests/test_util.cc', 'tests/test_swaglog.cc'],
|
||||
LIBS=[_common, 'json11', 'zmq', 'pthread'])
|
||||
|
||||
# Cython bindings
|
||||
params_python = envCython.Program('params_pyx.so', 'params_pyx.pyx', LIBS=envCython['LIBS'] + [_common, 'zmq', 'json11'])
|
||||
|
||||
params_python = env.SharedLibrary('params_c', 'params_c.cc', LIBS=[_common, 'zmq', 'json11'])
|
||||
common_python = [params_python]
|
||||
|
||||
Export('common_python')
|
||||
|
||||
if GetOption('extras'):
|
||||
env.Program('tests/test_swaglog', 'tests/test_swaglog.cc', LIBS=[_common, 'json11', 'zmq', 'pthread'])
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
from openpilot.common.esim.base import LPABase, Profile
|
||||
|
||||
@@ -27,6 +29,17 @@ def print_profiles(lpa: LPABase) -> None:
|
||||
print(f'{i}. {p.iccid} (nickname: {p.nickname or "<none provided>"}) (provider: {p.provider}) - {"enabled" if p.enabled else "disabled"}')
|
||||
|
||||
|
||||
def execute_and_process_notifications(lpa: LPABase, operation) -> None:
|
||||
try:
|
||||
operation()
|
||||
finally:
|
||||
time.sleep(1) # Need to wait for 1s after the operation is finished so the eUICC/modem can settle down.
|
||||
try:
|
||||
lpa.process_notifications()
|
||||
except Exception as e:
|
||||
print(f'failed to process eSIM notifications: {e}', file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(prog='esim.py', description='manage eSIM profiles on your comma device', epilog='comma.ai')
|
||||
sub = parser.add_subparsers(dest='cmd')
|
||||
@@ -50,18 +63,21 @@ if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
|
||||
lpa = HARDWARE.get_sim_lpa()
|
||||
if not lpa.is_euicc():
|
||||
raise SystemExit("no eUICC detected")
|
||||
if args.cmd == 'switch':
|
||||
lpa.switch_profile(resolve_iccid(lpa, args.profile))
|
||||
iccid = resolve_iccid(lpa, args.profile)
|
||||
execute_and_process_notifications(lpa, lambda: lpa.switch_profile(iccid))
|
||||
elif args.cmd == 'delete':
|
||||
iccid = resolve_iccid(lpa, args.profile)
|
||||
confirm = input(f'are you sure you want to delete profile {iccid}? (y/N) ')
|
||||
if confirm == 'y':
|
||||
lpa.delete_profile(iccid)
|
||||
execute_and_process_notifications(lpa, lambda: lpa.delete_profile(iccid))
|
||||
else:
|
||||
print('cancelled')
|
||||
exit(0)
|
||||
elif args.cmd == 'download':
|
||||
lpa.download_profile(args.qr, args.name)
|
||||
execute_and_process_notifications(lpa, lambda: lpa.download_profile(args.qr, args.name))
|
||||
elif args.cmd == 'nickname':
|
||||
lpa.nickname_profile(resolve_iccid(lpa, args.profile), args.name)
|
||||
else:
|
||||
|
||||
@@ -451,7 +451,7 @@ def process_notifications(client: AtClient) -> None:
|
||||
response = es10x_command(client, request)
|
||||
content = require_tag(require_tag(response, TAG_RETRIEVE_NOTIFICATION, "RetrieveNotificationsListResponse"),
|
||||
TAG_OK, "RetrieveNotificationsListResponse")
|
||||
pending_notif = next((v for t, v in iter_tlv(content) if t in (TAG_PROFILE_INSTALL_RESULT, 0x30)), None)
|
||||
pending_notif = next((content[start:end] for t, _, start, end in iter_tlv(content, with_positions=True) if t in (TAG_PROFILE_INSTALL_RESULT, 0x30)), None)
|
||||
if pending_notif is None:
|
||||
raise RuntimeError("Missing PendingNotification")
|
||||
|
||||
@@ -682,7 +682,7 @@ def download_profile(client: AtClient, activation_code: str) -> str:
|
||||
session.close()
|
||||
|
||||
|
||||
class TiciLPA(LPABase):
|
||||
class LPA(LPABase):
|
||||
def __init__(self):
|
||||
if hasattr(self, '_client'):
|
||||
return
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import secrets
|
||||
import struct
|
||||
from collections.abc import Callable, Sequence
|
||||
from functools import wraps
|
||||
from typing import Any, TypeVar
|
||||
|
||||
import capnp
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
_EDGE_SLOTS = 16
|
||||
_MINIMAL_EXAMPLES = 10
|
||||
_INTEGER_RANGES = {
|
||||
"int8": (-2**7, 2**7 - 1),
|
||||
"int16": (-2**15, 2**15 - 1),
|
||||
"int32": (-2**31, 2**31 - 1),
|
||||
"int64": (-2**63, 2**63 - 1),
|
||||
"uint8": (0, 2**8 - 1),
|
||||
"uint16": (0, 2**16 - 1),
|
||||
"uint32": (0, 2**32 - 1),
|
||||
"uint64": (0, 2**64 - 1),
|
||||
}
|
||||
|
||||
# One seed is shared by the whole test process. Individual tests derive their seed
|
||||
# from their unittest ID, so FUZZ_SEED is reproducible under the parallel runner too.
|
||||
FUZZ_SEED = int(os.environ.get("FUZZ_SEED", secrets.randbits(64)))
|
||||
|
||||
|
||||
class Fuzzy:
|
||||
"""Fast, deterministic data generator with systematic boundary coverage."""
|
||||
|
||||
def __init__(self, seed: int | str, example_index: int):
|
||||
self.example_index = example_index
|
||||
self._random = random.Random(seed)
|
||||
self._draw_index = 0
|
||||
|
||||
def _draw(self, edges: Sequence[T], random_value: Callable[[], T]) -> T:
|
||||
draw_index = self._draw_index
|
||||
self._draw_index += 1
|
||||
|
||||
# Preserve the cheap minimal prefix Hypothesis produced, then interleave
|
||||
# systematic boundaries and random values at every draw site.
|
||||
if self.example_index < _MINIMAL_EXAMPLES:
|
||||
return edges[0]
|
||||
search_example = self.example_index - _MINIMAL_EXAMPLES
|
||||
if search_example < _EDGE_SLOTS * 2 and search_example % 2 == 0:
|
||||
return edges[(search_example // 2 + draw_index) % len(edges)]
|
||||
if self._random.randrange(4) == 0:
|
||||
return self._random.choice(edges)
|
||||
return random_value()
|
||||
|
||||
def boolean(self) -> bool:
|
||||
return self._draw((False, True), lambda: bool(self._random.getrandbits(1)))
|
||||
|
||||
def choice(self, values: Sequence[T]) -> T:
|
||||
if not values:
|
||||
raise ValueError("cannot choose from an empty sequence")
|
||||
return self._draw(values, lambda: self._random.choice(values))
|
||||
|
||||
def integer(self, min_value: int, max_value: int) -> int:
|
||||
if min_value > max_value:
|
||||
raise ValueError(f"{min_value=} must not exceed {max_value=}")
|
||||
|
||||
edges = [
|
||||
0, 1, -1, min_value, max_value,
|
||||
min_value + 1, max_value - 1,
|
||||
]
|
||||
edges.extend(1 << bit for bit in range(max_value.bit_length()))
|
||||
edges.extend(-(1 << bit) for bit in range((-min_value).bit_length()))
|
||||
valid_edges = tuple(dict.fromkeys(v for v in edges if min_value <= v <= max_value))
|
||||
return self._draw(valid_edges, lambda: self._random.randint(min_value, max_value))
|
||||
|
||||
def floating(self, width: int = 64, *, allow_nan: bool = True, allow_infinity: bool = True) -> float:
|
||||
if width not in (32, 64):
|
||||
raise ValueError("float width must be 32 or 64")
|
||||
|
||||
if width == 32:
|
||||
unpack_format = "!f"
|
||||
finite_edges = (
|
||||
0.0, -0.0, 1.0, -1.0,
|
||||
struct.unpack(unpack_format, b"\x00\x00\x00\x01")[0],
|
||||
struct.unpack(unpack_format, b"\x80\x00\x00\x01")[0],
|
||||
struct.unpack(unpack_format, b"\x7f\x7f\xff\xff")[0],
|
||||
struct.unpack(unpack_format, b"\xff\x7f\xff\xff")[0],
|
||||
struct.unpack(unpack_format, b"\x00\x80\x00\x00")[0],
|
||||
struct.unpack(unpack_format, b"\x80\x80\x00\x00")[0],
|
||||
)
|
||||
else:
|
||||
unpack_format = "!d"
|
||||
finite_edges = (
|
||||
0.0, -0.0, 1.0, -1.0,
|
||||
math.ulp(0.0), -math.ulp(0.0),
|
||||
float.fromhex("0x1.fffffffffffffp+1023"), -float.fromhex("0x1.fffffffffffffp+1023"),
|
||||
float.fromhex("0x1p-1022"), -float.fromhex("0x1p-1022"),
|
||||
)
|
||||
|
||||
edges = list(finite_edges)
|
||||
if allow_infinity:
|
||||
edges.extend((math.inf, -math.inf))
|
||||
if allow_nan:
|
||||
edges.append(math.nan)
|
||||
|
||||
def random_float() -> float:
|
||||
while True:
|
||||
value = struct.unpack(unpack_format, self._random.randbytes(width // 8))[0]
|
||||
if (allow_nan or not math.isnan(value)) and (allow_infinity or not math.isinf(value)):
|
||||
return value
|
||||
|
||||
return self._draw(tuple(edges), random_float)
|
||||
|
||||
def _length(self, min_length: int, max_length: int | None) -> int:
|
||||
if min_length < 0:
|
||||
raise ValueError("minimum length must be non-negative")
|
||||
if max_length is not None and min_length > max_length:
|
||||
raise ValueError(f"{min_length=} must not exceed {max_length=}")
|
||||
if max_length == min_length:
|
||||
return min_length
|
||||
|
||||
offsets = (0, 1, 2, 4, 8, 16, 32)
|
||||
edges = tuple(min_length + offset for offset in offsets if max_length is None or min_length + offset <= max_length)
|
||||
|
||||
def random_length() -> int:
|
||||
# A geometric tail keeps ordinary examples small without placing an
|
||||
# artificial ceiling on an unbounded list.
|
||||
length = min_length
|
||||
while max_length is None or length < max_length:
|
||||
if self._random.randrange(8) == 0:
|
||||
break
|
||||
length += 1
|
||||
return length
|
||||
|
||||
return self._draw(edges, random_length)
|
||||
|
||||
def binary(self, min_size: int = 0, max_size: int | None = None) -> bytes:
|
||||
size = self._length(min_size, max_size)
|
||||
patterns = (
|
||||
bytes(size),
|
||||
b"\xff" * size,
|
||||
(b"\xaa\x55" * ((size + 1) // 2))[:size],
|
||||
bytes(i & 0xff for i in range(size)),
|
||||
)
|
||||
return self._draw(patterns, lambda: self._random.randbytes(size))
|
||||
|
||||
def text(self, min_size: int = 0, max_size: int | None = None) -> str:
|
||||
size = self._length(min_size, max_size)
|
||||
|
||||
def scalar() -> str:
|
||||
value = self._random.randrange(0x110000 - 0x800)
|
||||
if value >= 0xd800:
|
||||
value += 0x800
|
||||
return chr(value)
|
||||
|
||||
patterns = (
|
||||
"",
|
||||
"a" * size,
|
||||
"\0" * size,
|
||||
"\U0010ffff" * size,
|
||||
)
|
||||
valid_patterns = tuple(value for value in patterns if len(value) == size)
|
||||
return self._draw(valid_patterns, lambda: "".join(scalar() for _ in range(size)))
|
||||
|
||||
def list(self, generate: Callable[[], T], min_size: int = 0, max_size: int | None = None) -> list[T]:
|
||||
return [generate() for _ in range(self._length(min_size, max_size))]
|
||||
|
||||
|
||||
def fuzzy_test(max_examples: int) -> Callable[[Callable[..., None]], Callable[..., None]]:
|
||||
"""Run a unittest method repeatedly with independent, reproducible fuzzy data."""
|
||||
max_examples = int(os.environ.get("MAX_EXAMPLES", max_examples))
|
||||
assert max_examples >= 1
|
||||
|
||||
def decorator(fn: Callable[..., None]) -> Callable[..., None]:
|
||||
@wraps(fn)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> None:
|
||||
test_seed = f"{FUZZ_SEED}:{args[0].id()}"
|
||||
selected_example = os.environ.get("FUZZ_EXAMPLE")
|
||||
examples = [int(selected_example, 0)] if selected_example is not None else range(max_examples)
|
||||
|
||||
for example_index in examples:
|
||||
if not 0 <= example_index < max_examples:
|
||||
raise ValueError(f"FUZZ_EXAMPLE={example_index} is outside [0, {max_examples})")
|
||||
try:
|
||||
fn(*args, **kwargs, fuzzy=Fuzzy(f"{test_seed}:{example_index}", example_index))
|
||||
except Exception as exc:
|
||||
exc.add_note(f"reproduce with FUZZ_SEED={FUZZ_SEED} FUZZ_EXAMPLE={example_index}")
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def capnp_random_dict(fuzzy: Fuzzy, schema: Any, event: str | None = None, *, real_floats: bool = False) -> dict[str, Any]:
|
||||
"""Generate a dictionary accepted by a pycapnp struct constructor."""
|
||||
|
||||
def native(type_name: str) -> bool | int | float | str | bytes:
|
||||
if type_name == "bool":
|
||||
return fuzzy.boolean()
|
||||
if type_name in _INTEGER_RANGES:
|
||||
return fuzzy.integer(*_INTEGER_RANGES[type_name])
|
||||
if type_name in ("float32", "float64"):
|
||||
return fuzzy.floating(width=int(type_name[-2:]), allow_nan=not real_floats, allow_infinity=not real_floats)
|
||||
if type_name == "text":
|
||||
return fuzzy.text(max_size=1000)
|
||||
if type_name == "anyPointer":
|
||||
return fuzzy.text()
|
||||
if type_name == "data":
|
||||
return fuzzy.binary(max_size=1000)
|
||||
raise NotImplementedError(f"invalid Cap'n Proto type: {type_name}")
|
||||
|
||||
def generate_field(field: Any) -> Any:
|
||||
def rec(field_type: Any, base_type: str) -> Any:
|
||||
type_name = field_type.which()
|
||||
if type_name == "struct":
|
||||
struct_schema = field.schema.elementType if base_type == "list" else field.schema
|
||||
return capnp_random_dict(fuzzy, struct_schema, real_floats=real_floats)
|
||||
if type_name == "list":
|
||||
return fuzzy.list(lambda: rec(field_type.list.elementType, "list"))
|
||||
if type_name == "enum":
|
||||
enum_schema = field.schema.elementType if base_type == "list" else field.schema
|
||||
return fuzzy.choice(tuple(enum_schema.enumerants))
|
||||
return native(type_name)
|
||||
|
||||
try:
|
||||
if hasattr(field.proto, "slot"):
|
||||
slot_type = field.proto.slot.type
|
||||
return rec(slot_type, slot_type.which())
|
||||
return capnp_random_dict(fuzzy, field.schema, real_floats=real_floats)
|
||||
except capnp.lib.capnp.KjException:
|
||||
return capnp_random_dict(fuzzy, field.schema, real_floats=real_floats)
|
||||
|
||||
union_field = event or (fuzzy.choice(tuple(schema.union_fields)) if schema.union_fields else None)
|
||||
fields = schema.non_union_fields + ((union_field,) if union_field else ())
|
||||
return {
|
||||
field_name: generate_field(schema.fields[field_name])
|
||||
for field_name in fields
|
||||
if not field_name.endswith("DEPRECATED") and field_name != "deprecated"
|
||||
}
|
||||
@@ -31,12 +31,3 @@ def get_origin(cwd: str | None = None) -> str:
|
||||
return run_cmd(["git", "config", "remote." + tracking_remote + ".url"], cwd=cwd)
|
||||
except subprocess.CalledProcessError: # Not on a branch, fallback
|
||||
return run_cmd_default(["git", "config", "--get", "remote.origin.url"], cwd=cwd)
|
||||
|
||||
|
||||
@cache
|
||||
def get_normalized_origin(cwd: str | None = None) -> str:
|
||||
return get_origin(cwd) \
|
||||
.replace("git@", "", 1) \
|
||||
.replace(".git", "", 1) \
|
||||
.replace("https://", "", 1) \
|
||||
.replace(":", "/", 1)
|
||||
|
||||
@@ -2,15 +2,15 @@ import os
|
||||
from typing import cast
|
||||
|
||||
from openpilot.common.hardware.base import HardwareBase
|
||||
from openpilot.common.hardware.tici.hardware import Tici
|
||||
from openpilot.common.hardware.pc.hardware import Pc
|
||||
from openpilot.common.hardware.comma.hardware import HardwareComma
|
||||
from openpilot.common.hardware.pc.hardware import HardwarePc
|
||||
|
||||
TICI = os.path.isfile('/TICI')
|
||||
AGNOS = os.path.isfile('/AGNOS')
|
||||
PC = not TICI
|
||||
COMMA_HARDWARE = AGNOS
|
||||
PC = not COMMA_HARDWARE
|
||||
|
||||
|
||||
if TICI:
|
||||
HARDWARE = cast(HardwareBase, Tici())
|
||||
if COMMA_HARDWARE:
|
||||
HARDWARE = cast(HardwareBase, HardwareComma())
|
||||
else:
|
||||
HARDWARE = cast(HardwareBase, Pc())
|
||||
HARDWARE = cast(HardwareBase, HardwarePc())
|
||||
|
||||
+16
-16
@@ -23,14 +23,14 @@
|
||||
},
|
||||
{
|
||||
"name": "abl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/abl-b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c.img.xz",
|
||||
"hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c",
|
||||
"hash_raw": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/abl-29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35.img.xz",
|
||||
"hash": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35",
|
||||
"hash_raw": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35",
|
||||
"size": 274432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c"
|
||||
"ondevice_hash": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35"
|
||||
},
|
||||
{
|
||||
"name": "aop",
|
||||
@@ -56,29 +56,29 @@
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39.img.xz",
|
||||
"hash": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39",
|
||||
"hash_raw": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39",
|
||||
"size": 17487872,
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
|
||||
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||
"size": 46897152,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "ddfe93cc6a8531af92ee331d9bbaeae2f1d933bdb38e579769dc9fe7998eb626"
|
||||
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img.xz",
|
||||
"hash": "4dc41c2c072f5f5d5cd484cd6173049cd96acfb9a67bc20049775585fe881539",
|
||||
"hash_raw": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
|
||||
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
|
||||
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "cf1229630b7a2b8497705bca4ba947dbf0c217418ff4febff571aa4f4a878134",
|
||||
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
|
||||
"alt": {
|
||||
"hash": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img",
|
||||
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
|
||||
"size": 4718592000
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -12,7 +12,7 @@ import requests
|
||||
|
||||
SPARSE_CHUNK_FMT = struct.Struct('H2xI4x')
|
||||
|
||||
AGNOS_MANIFEST_FILE = "openpilot/system/hardware/tici/agnos.json"
|
||||
AGNOS_MANIFEST_FILE = "openpilot/system/hardware/comma/agnos.json"
|
||||
|
||||
|
||||
class StreamingDecompressor:
|
||||
+19
-19
@@ -339,51 +339,51 @@
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8.img.xz",
|
||||
"hash": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8",
|
||||
"hash_raw": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8",
|
||||
"size": 17487872,
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-5463317ca7a231f2ddd55a667a8c2b0d6692982359773ca875a99a0e6fd67fb4.img.xz",
|
||||
"hash": "5463317ca7a231f2ddd55a667a8c2b0d6692982359773ca875a99a0e6fd67fb4",
|
||||
"hash_raw": "5463317ca7a231f2ddd55a667a8c2b0d6692982359773ca875a99a0e6fd67fb4",
|
||||
"size": 46897152,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8"
|
||||
"ondevice_hash": "daefa6f897bdcc277e50dbf562fd63e57d50d9fc7a599f2d171208a490e725d9"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img.xz",
|
||||
"hash": "78acfe16a7b62a3a91fc7a81f40a693e4468cec1c69df7d0b1e550aacc646113",
|
||||
"hash_raw": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d.img.xz",
|
||||
"hash": "14ab660bb955604ab6c482b3f59e5239e2c40c54e798201afcaed43d5cb0dcfd",
|
||||
"hash_raw": "2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d",
|
||||
"size": 4718592000,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342",
|
||||
"ondevice_hash": "18666ca3b529304f5e7a8fc2e3c9dce715b55941e3965a7af43c613f8e18cfce",
|
||||
"alt": {
|
||||
"hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img",
|
||||
"hash": "2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d.img",
|
||||
"size": 4718592000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userdata_90",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-14a3fc6e9bd148b9deebf6ae9df2f1b3b759e629b337e41b6895864cdd51f630.img.xz",
|
||||
"hash": "52160dd01b30b3dc572226e8d549a034b03bc328e80f1f4cd6a857b6dd447687",
|
||||
"hash_raw": "14a3fc6e9bd148b9deebf6ae9df2f1b3b759e629b337e41b6895864cdd51f630",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-871adca25cb254ad1704ce427f3faaae1027f74aff43a3e150188c8a1c3d3cb8.img.xz",
|
||||
"hash": "e4f59e5dcf4758759da9284479163e420febf1c118bf33c83b402e80e241608d",
|
||||
"hash_raw": "871adca25cb254ad1704ce427f3faaae1027f74aff43a3e150188c8a1c3d3cb8",
|
||||
"size": 96636764160,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "3bbc052c7793087946b0cd668c1778f930084f6b00896aeebd193dfce53fa518"
|
||||
"ondevice_hash": "17b4ec7449e62c52be5df551542684b953e5c7ffc95103008043602f79ae7112"
|
||||
},
|
||||
{
|
||||
"name": "userdata_89",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-425c69d021f4ee2f767963bf7b991d1a492485fa465c5f5d001e1cf7de3d62a0.img.xz",
|
||||
"hash": "02a8c5512754d7781d930d242be3fad01fbce652297c69dfe8722dc92b18dc09",
|
||||
"hash_raw": "425c69d021f4ee2f767963bf7b991d1a492485fa465c5f5d001e1cf7de3d62a0",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-9d84bd64ef237a32675f936594c16932f8acbb52cf2dd82373a0e718cd32dfcf.img.xz",
|
||||
"hash": "652e61ecb5b12250dbfeb9f12fd74af16b867b5ea9bf8a08b5731ffd63a76317",
|
||||
"hash_raw": "9d84bd64ef237a32675f936594c16932f8acbb52cf2dd82373a0e718cd32dfcf",
|
||||
"size": 95563022336,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "3667d500f91b08a4671d7c09623eb6ae8fc905d9273893b50689e214005c7fa6"
|
||||
"ondevice_hash": "acad63821bb48823c871910b1a363ab924d2635a7574cc5045e877c438d3012f"
|
||||
}
|
||||
]
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
#include "common/util.h"
|
||||
#include "common/hardware/base.h"
|
||||
|
||||
class HardwareTici : public HardwareNone {
|
||||
class HardwareComma : public HardwareNone {
|
||||
public:
|
||||
static std::string get_name() {
|
||||
static const std::string name = []() {
|
||||
+16
-6
@@ -12,9 +12,8 @@ from openpilot.common.utils import sudo_read, sudo_write
|
||||
from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action
|
||||
from openpilot.common.esim.base import LPABase
|
||||
from openpilot.common.hardware.base import HardwareBase, ThermalConfig, ThermalZone
|
||||
from openpilot.common.esim.lpa import TiciLPA
|
||||
from openpilot.common.hardware.tici.pins import GPIO
|
||||
from openpilot.common.hardware.tici.amplifier import Amplifier
|
||||
from openpilot.common.hardware.comma.pins import GPIO
|
||||
from openpilot.common.hardware.comma.amplifier import Amplifier
|
||||
|
||||
MODEM_STATE_PATH = "/dev/shm/modem"
|
||||
|
||||
@@ -58,7 +57,17 @@ def get_default_route_iface():
|
||||
routes = [(int(route[6]), route[0]) for line in f.readlines()[1:] if (route := line.split())[1] == "00000000" and int(route[3], 16) & 0x1]
|
||||
return min(routes)[1] if routes else None
|
||||
|
||||
class Tici(HardwareBase):
|
||||
class HardwareComma(HardwareBase):
|
||||
"""
|
||||
This platform covers the Snapdragon 845-based comma devices:
|
||||
- tici = comma three
|
||||
- tizi = comma 3X
|
||||
- mici = comma four
|
||||
|
||||
We strictly use only the device codenames in this codebase for
|
||||
consistency, though all user-facing UI should use the product names.
|
||||
"""
|
||||
|
||||
@cached_property
|
||||
def amplifier(self):
|
||||
if self.get_device_type() == "mici":
|
||||
@@ -145,7 +154,8 @@ class Tici(HardwareBase):
|
||||
}
|
||||
|
||||
def get_sim_lpa(self) -> LPABase:
|
||||
return TiciLPA()
|
||||
from openpilot.common.esim.lpa import LPA
|
||||
return LPA()
|
||||
|
||||
def get_imei(self):
|
||||
return self.get_modem_state().get('imei', '')
|
||||
@@ -413,7 +423,7 @@ class Tici(HardwareBase):
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
t = Tici()
|
||||
t = HardwareComma()
|
||||
t.initialize_hardware()
|
||||
t.set_power_save(False)
|
||||
print(t.get_sim_info())
|
||||
@@ -3,17 +3,19 @@ import fcntl
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import termios
|
||||
import time
|
||||
|
||||
from contextlib import contextmanager
|
||||
from ipaddress import IPv4Address, AddressValueError
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from openpilot.common.serial import Serial
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s.%(msecs)03d %(levelname)-7s modem: %(message)s",
|
||||
@@ -64,6 +66,39 @@ INITIAL_STATE: dict[str, object] = {
|
||||
}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _serial_port(port: str, baudrate: int):
|
||||
fd = os.open(port, os.O_RDWR | os.O_NOCTTY)
|
||||
try:
|
||||
attrs = termios.tcgetattr(fd)
|
||||
attrs[0] = 0
|
||||
attrs[1] = 0
|
||||
attrs[2] = termios.CLOCAL | termios.CREAD | termios.CS8
|
||||
attrs[3] = 0
|
||||
attrs[4] = attrs[5] = getattr(termios, f"B{baudrate}")
|
||||
attrs[6][termios.VMIN] = 0
|
||||
attrs[6][termios.VTIME] = 0
|
||||
termios.tcsetattr(fd, termios.TCSANOW, attrs)
|
||||
yield fd
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def _read_line(fd: int, timeout: float) -> bytes:
|
||||
data = bytearray()
|
||||
deadline = time.monotonic() + timeout
|
||||
while True:
|
||||
readable, _, _ = select.select([fd], [], [], max(0.0, deadline - time.monotonic()))
|
||||
if not readable:
|
||||
return bytes(data)
|
||||
byte = os.read(fd, 1)
|
||||
if not byte:
|
||||
return bytes(data)
|
||||
data.extend(byte)
|
||||
if byte == b"\n":
|
||||
return bytes(data)
|
||||
|
||||
|
||||
class State(Enum):
|
||||
INITIALIZING = "INITIALIZING"
|
||||
SEARCHING = "SEARCHING"
|
||||
@@ -97,10 +132,11 @@ class PPPSession:
|
||||
def reset_data_port():
|
||||
"""Drop DTR on PPP_PORT so the modem terminates any stuck PPP session."""
|
||||
try:
|
||||
with Serial(PPP_PORT, baudrate=460800, timeout=1) as s:
|
||||
s.dtr = False
|
||||
with _serial_port(PPP_PORT, 460800) as fd:
|
||||
dtr = struct.pack("I", termios.TIOCM_DTR)
|
||||
fcntl.ioctl(fd, termios.TIOCMBIC, dtr)
|
||||
time.sleep(0.2)
|
||||
s.dtr = True
|
||||
fcntl.ioctl(fd, termios.TIOCMBIS, dtr)
|
||||
except Exception as e:
|
||||
logging.warning(f"data port reset failed: {e}")
|
||||
|
||||
@@ -221,12 +257,14 @@ class Modem:
|
||||
os.close(fd)
|
||||
return []
|
||||
try:
|
||||
with Serial(AT_PORT, baudrate=9600, timeout=5) as ser:
|
||||
ser.reset_input_buffer()
|
||||
ser.write((cmd + "\r").encode())
|
||||
with _serial_port(AT_PORT, 9600) as serial_fd:
|
||||
termios.tcflush(serial_fd, termios.TCIFLUSH)
|
||||
command = (cmd + "\r").encode()
|
||||
while command:
|
||||
command = command[os.write(serial_fd, command):]
|
||||
lines = []
|
||||
while True:
|
||||
raw = ser.readline()
|
||||
raw = _read_line(serial_fd, 5)
|
||||
if not raw:
|
||||
raise TimeoutError("AT timeout")
|
||||
line = raw.decode(errors="ignore").strip()
|
||||
+2
-1
@@ -6,7 +6,8 @@ TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
||||
MANIFEST = os.path.join(TEST_DIR, "../agnos.json")
|
||||
|
||||
|
||||
class TestAgnosUpdater:
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
class TestAgnosUpdater(OpenpilotTestCase):
|
||||
|
||||
def test_manifest(self):
|
||||
with open(MANIFEST) as f:
|
||||
Regular → Executable
+13
-10
@@ -1,18 +1,17 @@
|
||||
import pytest
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import time
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from panda import Panda
|
||||
from openpilot.common.hardware import TICI, HARDWARE
|
||||
from openpilot.common.hardware.tici.amplifier import Amplifier
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.hardware import HARDWARE
|
||||
from openpilot.common.hardware.comma.amplifier import Amplifier
|
||||
|
||||
|
||||
class TestAmplifier:
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
if not TICI:
|
||||
pytest.skip()
|
||||
class TestAmplifier(OpenpilotTestCase):
|
||||
COMMA_HARDWARE_TEST = True
|
||||
|
||||
def setup_method(self):
|
||||
# clear dmesg
|
||||
@@ -65,4 +64,8 @@ class TestAmplifier:
|
||||
if self._check_for_i2c_errors(True):
|
||||
break
|
||||
else:
|
||||
pytest.fail("didn't hit any i2c errors")
|
||||
self.fail("didn't hit any i2c errors")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -5,9 +5,9 @@
|
||||
#include "common/hardware/base.h"
|
||||
#include "common/util.h"
|
||||
|
||||
#if __TICI__
|
||||
#include "common/hardware/tici/hardware.h"
|
||||
#define Hardware HardwareTici
|
||||
#if __COMMA_HARDWARE__
|
||||
#include "common/hardware/comma/hardware.h"
|
||||
#define Hardware HardwareComma
|
||||
#else
|
||||
#include "common/hardware/pc/hardware.h"
|
||||
#define Hardware HardwarePC
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from openpilot.cereal import log
|
||||
from openpilot.common.hardware.base import HardwareBase
|
||||
|
||||
class Pc(HardwareBase):
|
||||
class HardwarePc(HardwareBase):
|
||||
def get_device_type(self):
|
||||
return "pc"
|
||||
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
CHESTNUT_VENDOR_ID = 0xADD1
|
||||
CHESTNUT_PRODUCT_ID = 0x0001
|
||||
CHESTNUT_FW_VERSION = "ed4e39b7"
|
||||
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
|
||||
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
|
||||
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
|
||||
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
|
||||
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
|
||||
|
||||
|
||||
def get_usb_topology() -> set[str]:
|
||||
try:
|
||||
return set(os.listdir(USB_DEVICES_PATH))
|
||||
except OSError:
|
||||
return set()
|
||||
|
||||
|
||||
def read(path: Path) -> str | None:
|
||||
@@ -15,7 +26,7 @@ def read(path: Path) -> str | None:
|
||||
def read_int(path: Path, base: int = 10) -> int:
|
||||
try:
|
||||
return int(path.read_text(), base)
|
||||
except (OSError, ValueError):
|
||||
except (OSError, ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
|
||||
@@ -36,6 +47,7 @@ def controller(device: Path) -> Path | None:
|
||||
|
||||
def get_usb_state() -> list[dict]:
|
||||
devices = []
|
||||
typec_orientation = read_int(TYPEC_CC_ORIENTATION_PATH)
|
||||
for device in usb_devices():
|
||||
vendor_id = read_int(device / "idVendor", 16)
|
||||
product_id = read_int(device / "idProduct", 16)
|
||||
@@ -49,6 +61,7 @@ def get_usb_state() -> list[dict]:
|
||||
"manufacturer": read(device / "manufacturer") or "",
|
||||
"product": read(device / "product") or "",
|
||||
"linkErrorCount": read_int(ctrl / "portli", 0) & 0xFFFF if ctrl is not None else 0,
|
||||
"usb3Lane": {1: "a", 2: "b"}.get(typec_orientation, "unknown") if ctrl is not None and ctrl.name == PRIMARY_USB_CONTROLLER else "unknown",
|
||||
})
|
||||
return devices
|
||||
|
||||
@@ -66,8 +79,9 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
|
||||
entry.manufacturer = device["manufacturer"]
|
||||
entry.product = device["product"]
|
||||
entry.linkErrorCount = device["linkErrorCount"]
|
||||
entry.usb3Lane = device.get("usb3Lane", "unknown")
|
||||
|
||||
if (entry.vendorId, entry.productId) == (CHESTNUT_VENDOR_ID, CHESTNUT_PRODUCT_ID):
|
||||
if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS:
|
||||
chestnut_present = True
|
||||
|
||||
device_state.chestnutPresent = chestnut_present
|
||||
|
||||
@@ -8,12 +8,12 @@ import functools
|
||||
import threading
|
||||
from openpilot.cereal.messaging import PubMaster
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
from openpilot.common.mock.generators import generate_livePose
|
||||
from openpilot.common.mock.generators import generate_deviceMotion
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
|
||||
|
||||
MOCK_GENERATOR = {
|
||||
"livePose": generate_livePose
|
||||
"deviceMotion": generate_deviceMotion
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from openpilot.cereal import messaging
|
||||
|
||||
|
||||
def generate_livePose():
|
||||
msg = messaging.new_message('livePose')
|
||||
def generate_deviceMotion():
|
||||
msg = messaging.new_message('deviceMotion')
|
||||
meas = {'x': 0.0, 'y': 0.0, 'z': 0.0, 'xStd': 0.0, 'yStd': 0.0, 'zStd': 0.0, 'valid': True}
|
||||
msg.livePose.orientationNED = meas
|
||||
msg.livePose.velocityDevice = meas
|
||||
msg.livePose.angularVelocityDevice = meas
|
||||
msg.livePose.accelerationDevice = meas
|
||||
msg.livePose.inputsOK = True
|
||||
msg.livePose.posenetOK = True
|
||||
msg.livePose.sensorsOK = True
|
||||
msg.deviceMotion.orientationNED = meas
|
||||
msg.deviceMotion.velocityDevice = meas
|
||||
msg.deviceMotion.angularVelocityDevice = meas
|
||||
msg.deviceMotion.accelerationDevice = meas
|
||||
msg.deviceMotion.inputsOK = True
|
||||
msg.deviceMotion.posenetOK = True
|
||||
msg.deviceMotion.sensorsOK = True
|
||||
return msg
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import re
|
||||
import sys
|
||||
import pytest
|
||||
import inspect
|
||||
import unittest
|
||||
|
||||
|
||||
def _to_safe_name(s):
|
||||
@@ -10,22 +10,60 @@ def _to_safe_name(s):
|
||||
|
||||
class parameterized:
|
||||
@staticmethod
|
||||
def expand(cases):
|
||||
def expand(cases, names=None, ids=None, serial=False):
|
||||
cases = list(cases)
|
||||
|
||||
if not cases:
|
||||
return lambda func: pytest.mark.skip("no parameterized cases")(func)
|
||||
return lambda func: unittest.skip("no parameterized cases")(func)
|
||||
|
||||
def decorator(func):
|
||||
params = [p for p in inspect.signature(func).parameters if p != 'self']
|
||||
normalized = [c if isinstance(c, tuple) else (c,) for c in cases]
|
||||
# Infer arg count from first case so extra params (e.g. from @given) are left untouched
|
||||
expand_params = params[: len(normalized[0])]
|
||||
if len(expand_params) == 1:
|
||||
return pytest.mark.parametrize(expand_params[0], [c[0] for c in normalized])(func)
|
||||
return pytest.mark.parametrize(', '.join(expand_params), normalized)(func)
|
||||
if serial:
|
||||
def decorator(func):
|
||||
normalized = [case if isinstance(case, tuple) else (case,) for case in cases]
|
||||
|
||||
return decorator
|
||||
def wrapper(self):
|
||||
for case in normalized:
|
||||
with self.subTest():
|
||||
func(self, *case)
|
||||
|
||||
wrapper.__name__ = func.__name__
|
||||
wrapper.__doc__ = func.__doc__
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
return lambda func: _Expanded(func, cases, names, ids)
|
||||
|
||||
|
||||
class _Expanded:
|
||||
"""Descriptor that turns every parameter case into a real unittest method."""
|
||||
|
||||
def __init__(self, func, cases, names, ids):
|
||||
self.func = func
|
||||
self.cases = [c if isinstance(c, tuple) else (c,) for c in cases]
|
||||
self.names = names
|
||||
self.ids = ids
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
params = [p for p in inspect.signature(self.func).parameters if p != "self"]
|
||||
|
||||
for index, case in enumerate(self.cases):
|
||||
label = self.ids(*case) if self.ids is not None else None
|
||||
method_name = f"{name}_{index}" + (f"_{_to_safe_name(label)}" if label is not None else "")
|
||||
|
||||
def test_method(test_case, current_case=case):
|
||||
if self.names is None:
|
||||
self.func(test_case, *current_case)
|
||||
else:
|
||||
values = dict(zip(self.names, current_case, strict=True))
|
||||
values.update({param: test_case._fixture(param) for param in params if param not in values})
|
||||
self.func(test_case, **values)
|
||||
|
||||
test_method.__name__ = method_name
|
||||
test_method.__doc__ = self.func.__doc__
|
||||
setattr(owner, method_name, test_method)
|
||||
|
||||
# The descriptor itself is only a method factory, not a test.
|
||||
setattr(owner, name, None)
|
||||
|
||||
|
||||
def parameterized_class(attrs, input_list=None):
|
||||
@@ -39,16 +77,17 @@ def parameterized_class(attrs, input_list=None):
|
||||
def decorator(cls):
|
||||
globs = sys._getframe(1).f_globals
|
||||
for i, params in enumerate(params_list):
|
||||
# append sanitized string param values so pytest -k can filter by them
|
||||
# Append sanitized values so unittest's -k can filter by them.
|
||||
suffix = "_".join(filter(None, (_to_safe_name(v) for v in params.values() if isinstance(v, str))))
|
||||
name = f"{cls.__name__}_{i}" + (f"_{suffix}" if suffix else "")
|
||||
new_cls = type(name, (cls,), dict(params))
|
||||
new_cls.__module__ = cls.__module__
|
||||
new_cls.__test__ = True # override inherited False so pytest collects this subclass
|
||||
new_cls.__unittest_skip__ = False
|
||||
new_cls.__unittest_skip_why__ = "" # else inherited from the base and the collector drops it
|
||||
globs[name] = new_cls
|
||||
# Don't collect the un-parametrised base, but return it so outer decorators
|
||||
# (e.g. @pytest.mark.skip) land on it and propagate to subclasses via MRO.
|
||||
cls.__test__ = False
|
||||
# Don't collect the un-parametrised base.
|
||||
cls.__unittest_skip__ = True
|
||||
cls.__unittest_skip_why__ = "parameterized base class"
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
+209
-7
@@ -1,16 +1,218 @@
|
||||
from openpilot.common.params_pyx import Params, ParamKeyFlag, ParamKeyType, UnknownKeyName
|
||||
assert Params
|
||||
assert ParamKeyFlag
|
||||
assert ParamKeyType
|
||||
assert UnknownKeyName
|
||||
import sys
|
||||
import json
|
||||
import ctypes
|
||||
import weakref
|
||||
import builtins
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from enum import IntEnum, IntFlag
|
||||
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
|
||||
class ParamKeyFlag(IntFlag):
|
||||
PERSISTENT = 0x02
|
||||
CLEAR_ON_MANAGER_START = 0x04
|
||||
CLEAR_ON_ONROAD_TRANSITION = 0x08
|
||||
CLEAR_ON_OFFROAD_TRANSITION = 0x10
|
||||
DEVELOPMENT_ONLY = 0x40
|
||||
CLEAR_ON_IGNITION_ON = 0x80
|
||||
BACKUP = 0x100
|
||||
ALL = 0xFFFFFFFF
|
||||
|
||||
|
||||
class ParamKeyType(IntEnum):
|
||||
STRING = 0
|
||||
BOOL = 1
|
||||
INT = 2
|
||||
FLOAT = 3
|
||||
TIME = 4
|
||||
JSON = 5
|
||||
BYTES = 6
|
||||
|
||||
|
||||
_suffix = ".dylib" if sys.platform == "darwin" else ".so"
|
||||
lib = ctypes.CDLL(Path(__file__).with_name(f"libparams_c{_suffix}"))
|
||||
|
||||
ParamsHandle = ctypes.c_void_p
|
||||
|
||||
|
||||
class ParamsBuffer(ctypes.Structure):
|
||||
_fields_ = [("data", ctypes.c_void_p), ("size", ctypes.c_size_t)]
|
||||
|
||||
|
||||
def _bind_raw(name, args, result=None):
|
||||
function = getattr(lib, name)
|
||||
function.argtypes = args
|
||||
function.restype = result
|
||||
return function
|
||||
|
||||
|
||||
params_last_error = _bind_raw("params_last_error", [], ctypes.c_char_p)
|
||||
|
||||
|
||||
def _bind(name, args, result=None):
|
||||
function = _bind_raw(name, args, result)
|
||||
|
||||
def checked(*call_args):
|
||||
value = function(*call_args)
|
||||
if error := params_last_error():
|
||||
raise RuntimeError(error.decode())
|
||||
return value
|
||||
|
||||
return checked
|
||||
|
||||
|
||||
params_create = _bind("params_create", [ctypes.c_char_p, ctypes.c_size_t], ParamsHandle)
|
||||
params_destroy = _bind("params_destroy", [ParamsHandle])
|
||||
params_clear_all = _bind("params_clear_all", [ParamsHandle, ctypes.c_uint])
|
||||
params_check_key = _bind("params_check_key", [ParamsHandle, ctypes.c_char_p], ctypes.c_bool)
|
||||
params_get_key_type = _bind("params_get_key_type", [ParamsHandle, ctypes.c_char_p], ctypes.c_int)
|
||||
params_get_default = _bind("params_get_default", [ParamsHandle, ctypes.c_char_p], ParamsBuffer)
|
||||
params_get = _bind("params_get", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ParamsBuffer)
|
||||
params_get_bool = _bind("params_get_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ctypes.c_bool)
|
||||
params_put = _bind("params_put", [ParamsHandle, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_int)
|
||||
params_put_bool = _bind("params_put_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool, ctypes.c_bool], ctypes.c_int)
|
||||
params_remove = _bind("params_remove", [ParamsHandle, ctypes.c_char_p], ctypes.c_int)
|
||||
params_get_path = _bind("params_get_path", [ParamsHandle, ctypes.c_char_p, ctypes.c_size_t], ParamsBuffer)
|
||||
params_keys_size = _bind("params_keys_size", [ParamsHandle], ctypes.c_size_t)
|
||||
params_key_at = _bind("params_key_at", [ParamsHandle, ctypes.c_size_t], ParamsBuffer)
|
||||
params_keys_by_flag = _bind("params_keys_by_flag", [ParamsHandle, ctypes.c_uint, ctypes.POINTER(ParamsBuffer), ctypes.c_size_t], ctypes.c_size_t)
|
||||
|
||||
PYTHON_2_CPP = {
|
||||
(str, ParamKeyType.STRING): lambda v: v,
|
||||
(builtins.bool, ParamKeyType.BOOL): lambda v: "1" if v else "0",
|
||||
(int, ParamKeyType.INT): str,
|
||||
(float, ParamKeyType.FLOAT): str,
|
||||
(datetime.datetime, ParamKeyType.TIME): lambda v: v.isoformat(),
|
||||
(dict, ParamKeyType.JSON): json.dumps,
|
||||
(list, ParamKeyType.JSON): json.dumps,
|
||||
(bytes, ParamKeyType.BYTES): lambda v: v,
|
||||
}
|
||||
CPP_2_PYTHON = {
|
||||
ParamKeyType.STRING: lambda v: v.decode("utf-8"),
|
||||
ParamKeyType.BOOL: lambda v: v == b"1",
|
||||
ParamKeyType.INT: int,
|
||||
ParamKeyType.FLOAT: float,
|
||||
ParamKeyType.TIME: lambda v: datetime.datetime.fromisoformat(v.decode("utf-8")),
|
||||
ParamKeyType.JSON: json.loads,
|
||||
ParamKeyType.BYTES: lambda v: v,
|
||||
}
|
||||
|
||||
|
||||
def ensure_bytes(v):
|
||||
return v.encode() if isinstance(v, str) else v
|
||||
|
||||
|
||||
def _copy_string(value):
|
||||
if value.data is None:
|
||||
return None
|
||||
return ctypes.string_at(value.data, value.size)
|
||||
|
||||
|
||||
class UnknownKeyName(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Params:
|
||||
def __init__(self, d=""):
|
||||
path = ensure_bytes(d)
|
||||
self.p = params_create(path, len(path))
|
||||
self._finalizer = weakref.finalize(self, params_destroy, self.p)
|
||||
self._finalizer.atexit = False # daemon threads can still be using live Params handles during interpreter shutdown
|
||||
self.d = d
|
||||
|
||||
def __reduce__(self):
|
||||
return (type(self), (self.d,))
|
||||
|
||||
def clear_all(self, tx_flag=ParamKeyFlag.ALL):
|
||||
params_clear_all(self.p, int(tx_flag))
|
||||
|
||||
def check_key(self, key):
|
||||
key = ensure_bytes(key)
|
||||
if b"\0" in key or not params_check_key(self.p, key):
|
||||
raise UnknownKeyName(key)
|
||||
return key
|
||||
|
||||
def python2cpp(self, proposed_type, expected_type, value, key):
|
||||
cast = PYTHON_2_CPP.get((proposed_type, expected_type))
|
||||
if cast:
|
||||
return cast(value)
|
||||
raise TypeError(f"Type mismatch while writing param {key}: {proposed_type=} {expected_type=} {value=}")
|
||||
|
||||
def _cpp2python(self, t, value, default, key):
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return CPP_2_PYTHON[t](value)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
cloudlog.warning(f"Failed to cast param {key} with {value=} from type {t=}")
|
||||
return self._cpp2python(t, default, None, key)
|
||||
|
||||
def _default(self, key):
|
||||
return _copy_string(params_get_default(self.p, key))
|
||||
|
||||
def get(self, key, block=False, return_default=False):
|
||||
k = self.check_key(key)
|
||||
t = self.get_type(k)
|
||||
default = self._default(k) if return_default else None
|
||||
value = _copy_string(params_get(self.p, k, block))
|
||||
if value == b"":
|
||||
if block:
|
||||
raise KeyboardInterrupt
|
||||
return self._cpp2python(t, default, None, key)
|
||||
return self._cpp2python(t, value, default, key)
|
||||
|
||||
def get_bool(self, key, block=False):
|
||||
return bool(params_get_bool(self.p, self.check_key(key), block))
|
||||
|
||||
def _put_cast(self, key, dat):
|
||||
return ensure_bytes(self.python2cpp(type(dat), self.get_type(key), dat, key))
|
||||
|
||||
def put(self, key, dat, block=False):
|
||||
"""Write a parameter. block=True waits until it is persisted to disk."""
|
||||
k = self.check_key(key)
|
||||
value = self._put_cast(k, dat)
|
||||
params_put(self.p, k, value, len(value), block)
|
||||
|
||||
def put_bool(self, key, val, block=False):
|
||||
params_put_bool(self.p, self.check_key(key), val, block)
|
||||
|
||||
def remove(self, key):
|
||||
params_remove(self.p, self.check_key(key))
|
||||
|
||||
def get_param_path(self, key=""):
|
||||
key = ensure_bytes(key)
|
||||
return _copy_string(params_get_path(self.p, key, len(key))).decode()
|
||||
|
||||
def get_type(self, key):
|
||||
return ParamKeyType(params_get_key_type(self.p, self.check_key(key)))
|
||||
|
||||
def all_keys(self, flag=ParamKeyFlag.ALL):
|
||||
if flag == ParamKeyFlag.ALL:
|
||||
keys = []
|
||||
for i in range(params_keys_size(self.p)):
|
||||
keys.append(_copy_string(params_key_at(self.p, i)))
|
||||
return keys
|
||||
max_keys = 1024
|
||||
buf = (ParamsBuffer * max_keys)()
|
||||
count = params_keys_by_flag(self.p, int(flag), buf, max_keys)
|
||||
return [_copy_string(buf[i]) for i in range(min(count, max_keys))]
|
||||
|
||||
def get_default_value(self, key):
|
||||
k = self.check_key(key)
|
||||
return self._cpp2python(self.get_type(k), self._default(k), None, key)
|
||||
|
||||
def cpp2python(self, key, value):
|
||||
return self._cpp2python(self.get_type(key), value, None, key)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
params = Params()
|
||||
key = sys.argv[1]
|
||||
assert params.check_key(key), f"unknown param: {key}"
|
||||
|
||||
params.check_key(key)
|
||||
if len(sys.argv) == 3:
|
||||
val = sys.argv[2]
|
||||
print(f"SET: {key} = {val}")
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "common/params.h"
|
||||
|
||||
typedef struct {
|
||||
const char *data;
|
||||
size_t size;
|
||||
} ParamsBuffer;
|
||||
|
||||
struct ParamsHandle {
|
||||
ParamsHandle(const char *path, size_t path_size) : params(std::string(path, path_size)), keys(params.allKeys()) {
|
||||
}
|
||||
|
||||
Params params;
|
||||
const std::vector<std::string> keys;
|
||||
};
|
||||
|
||||
namespace {
|
||||
thread_local char last_error[512] = {};
|
||||
thread_local std::string result;
|
||||
|
||||
void set_error(const char *error) {
|
||||
snprintf(last_error, sizeof(last_error), "%s", error);
|
||||
}
|
||||
|
||||
ParamsBuffer return_string(std::string value) {
|
||||
result = std::move(value);
|
||||
return {result.data(), result.size()};
|
||||
}
|
||||
|
||||
template <typename Result, typename Callable>
|
||||
Result translate_exceptions(Result failure, Callable &&callable) noexcept {
|
||||
last_error[0] = '\0';
|
||||
try {
|
||||
return callable();
|
||||
} catch (const std::exception &e) {
|
||||
set_error(e.what());
|
||||
} catch (...) {
|
||||
set_error("unknown C++ exception");
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
template <typename Callable>
|
||||
void translate_exceptions(Callable &&callable) noexcept {
|
||||
translate_exceptions(false, [&]() {
|
||||
callable();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
ParamsHandle *params_create(const char *path, size_t path_size) noexcept {
|
||||
return translate_exceptions(static_cast<ParamsHandle *>(nullptr), [&]() {
|
||||
return new ParamsHandle(path, path_size);
|
||||
});
|
||||
}
|
||||
|
||||
void params_destroy(ParamsHandle *handle) noexcept {
|
||||
translate_exceptions([&]() {
|
||||
delete handle;
|
||||
});
|
||||
}
|
||||
|
||||
const char *params_last_error() noexcept {
|
||||
return last_error;
|
||||
}
|
||||
|
||||
void params_clear_all(ParamsHandle *handle, unsigned int flag) noexcept {
|
||||
translate_exceptions([&]() {
|
||||
handle->params.clearAll(static_cast<ParamKeyFlag>(flag));
|
||||
});
|
||||
}
|
||||
|
||||
bool params_check_key(ParamsHandle *handle, const char *key) noexcept {
|
||||
return translate_exceptions(false, [&]() {
|
||||
return handle->params.checkKey(key);
|
||||
});
|
||||
}
|
||||
|
||||
int params_get_key_type(ParamsHandle *handle, const char *key) noexcept {
|
||||
return translate_exceptions(-1, [&]() {
|
||||
return static_cast<int>(handle->params.getKeyType(key));
|
||||
});
|
||||
}
|
||||
|
||||
ParamsBuffer params_get_default(ParamsHandle *handle, const char *key) noexcept {
|
||||
return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() {
|
||||
auto value = handle->params.getKeyDefaultValue(key);
|
||||
if (!value.has_value()) {
|
||||
return ParamsBuffer{nullptr, 0};
|
||||
}
|
||||
return return_string(*value);
|
||||
});
|
||||
}
|
||||
|
||||
ParamsBuffer params_get(ParamsHandle *handle, const char *key, bool block) noexcept {
|
||||
return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() {
|
||||
return return_string(handle->params.get(key, block));
|
||||
});
|
||||
}
|
||||
|
||||
bool params_get_bool(ParamsHandle *handle, const char *key, bool block) noexcept {
|
||||
return translate_exceptions(false, [&]() {
|
||||
return handle->params.getBool(key, block);
|
||||
});
|
||||
}
|
||||
|
||||
int params_put(ParamsHandle *handle, const char *key, const char *value, size_t size, bool block) noexcept {
|
||||
return translate_exceptions(-1, [&]() {
|
||||
if (block) {
|
||||
return handle->params.put(key, value, size);
|
||||
}
|
||||
handle->params.putNonBlocking(key, std::string(value, size));
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
int params_put_bool(ParamsHandle *handle, const char *key, bool value, bool block) noexcept {
|
||||
return translate_exceptions(-1, [&]() {
|
||||
if (block) {
|
||||
return handle->params.putBool(key, value);
|
||||
}
|
||||
handle->params.putBoolNonBlocking(key, value);
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
int params_remove(ParamsHandle *handle, const char *key) noexcept {
|
||||
return translate_exceptions(-1, [&]() {
|
||||
return handle->params.remove(key);
|
||||
});
|
||||
}
|
||||
|
||||
ParamsBuffer params_get_path(ParamsHandle *handle, const char *key, size_t key_size) noexcept {
|
||||
return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() {
|
||||
return return_string(handle->params.getParamPath(std::string(key, key_size)));
|
||||
});
|
||||
}
|
||||
|
||||
size_t params_keys_size(ParamsHandle *handle) noexcept {
|
||||
return translate_exceptions(size_t{0}, [&]() {
|
||||
return handle->keys.size();
|
||||
});
|
||||
}
|
||||
|
||||
ParamsBuffer params_key_at(ParamsHandle *handle, size_t index) noexcept {
|
||||
return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() {
|
||||
if (index >= handle->keys.size()) {
|
||||
return ParamsBuffer{nullptr, 0};
|
||||
}
|
||||
return return_string(handle->keys[index]);
|
||||
});
|
||||
}
|
||||
|
||||
size_t params_keys_by_flag(ParamsHandle *handle, uint32_t flag, ParamsBuffer *out, size_t out_size) noexcept {
|
||||
return translate_exceptions(size_t{0}, [&]() {
|
||||
auto filtered = handle->params.allKeys(static_cast<ParamKeyFlag>(flag));
|
||||
size_t count = std::min(filtered.size(), out_size);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
out[i] = return_string(filtered[i]);
|
||||
}
|
||||
return filtered.size();
|
||||
});
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -106,7 +106,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"PandaHeartbeatLost", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||
{"PrimeType", {PERSISTENT, INT}},
|
||||
{"RecordAudio", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"RecordAudioFeedback", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RecordFront", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet
|
||||
{"SecOCKey", {PERSISTENT | DONT_LOG | BACKUP, STRING}},
|
||||
@@ -131,8 +130,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"UpdaterLastFetchTime", {PERSISTENT, TIME}},
|
||||
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"UsbGpuPresent", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||
{"UsbGpuCompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||
{"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
# distutils: language = c++
|
||||
# cython: language_level = 3
|
||||
import builtins
|
||||
import datetime
|
||||
import json
|
||||
from libcpp cimport bool
|
||||
from libcpp.string cimport string
|
||||
from libcpp.vector cimport vector
|
||||
from libcpp.optional cimport optional
|
||||
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
cdef extern from "common/params.h":
|
||||
cpdef enum ParamKeyFlag:
|
||||
PERSISTENT
|
||||
CLEAR_ON_MANAGER_START
|
||||
CLEAR_ON_ONROAD_TRANSITION
|
||||
CLEAR_ON_OFFROAD_TRANSITION
|
||||
DEVELOPMENT_ONLY
|
||||
CLEAR_ON_IGNITION_ON
|
||||
BACKUP
|
||||
ALL
|
||||
|
||||
cpdef enum ParamKeyType:
|
||||
STRING
|
||||
BOOL
|
||||
INT
|
||||
FLOAT
|
||||
TIME
|
||||
JSON
|
||||
BYTES
|
||||
|
||||
cdef cppclass c_Params "Params":
|
||||
c_Params(string) except + nogil
|
||||
string get(string, bool) nogil
|
||||
bool getBool(string, bool) nogil
|
||||
int remove(string) nogil
|
||||
int put(string, string) nogil
|
||||
void putNonBlocking(string, string) nogil
|
||||
void putBoolNonBlocking(string, bool) nogil
|
||||
int putBool(string, bool) nogil
|
||||
bool checkKey(string) nogil
|
||||
ParamKeyType getKeyType(string) nogil
|
||||
optional[string] getKeyDefaultValue(string) nogil
|
||||
string getParamPath(string) nogil
|
||||
void clearAll(ParamKeyFlag)
|
||||
vector[string] allKeys(ParamKeyFlag)
|
||||
|
||||
PYTHON_2_CPP = {
|
||||
(str, STRING): lambda v: v,
|
||||
(builtins.bool, BOOL): lambda v: "1" if v else "0",
|
||||
(int, INT): str,
|
||||
(float, FLOAT): str,
|
||||
(datetime.datetime, TIME): lambda v: v.isoformat(),
|
||||
(dict, JSON): json.dumps,
|
||||
(list, JSON): json.dumps,
|
||||
(bytes, BYTES): lambda v: v,
|
||||
}
|
||||
CPP_2_PYTHON = {
|
||||
STRING: lambda v: v.decode("utf-8"),
|
||||
BOOL: lambda v: v == b"1",
|
||||
INT: int,
|
||||
FLOAT: float,
|
||||
TIME: lambda v: datetime.datetime.fromisoformat(v.decode("utf-8")),
|
||||
JSON: json.loads,
|
||||
BYTES: lambda v: v,
|
||||
}
|
||||
|
||||
def ensure_bytes(v):
|
||||
return v.encode() if isinstance(v, str) else v
|
||||
|
||||
class UnknownKeyName(Exception):
|
||||
pass
|
||||
|
||||
cdef class Params:
|
||||
cdef c_Params* p
|
||||
cdef str d
|
||||
|
||||
def __cinit__(self, d=""):
|
||||
cdef string path = <string>d.encode()
|
||||
with nogil:
|
||||
self.p = new c_Params(path)
|
||||
self.d = d
|
||||
|
||||
def __reduce__(self):
|
||||
return (type(self), (self.d,))
|
||||
|
||||
def __dealloc__(self):
|
||||
del self.p
|
||||
|
||||
def clear_all(self, tx_flag=ParamKeyFlag.ALL):
|
||||
self.p.clearAll(tx_flag)
|
||||
|
||||
def check_key(self, key):
|
||||
key = ensure_bytes(key)
|
||||
if not self.p.checkKey(key):
|
||||
raise UnknownKeyName(key)
|
||||
return key
|
||||
|
||||
def python2cpp(self, proposed_type, expected_type, value, key):
|
||||
cast = PYTHON_2_CPP.get((proposed_type, expected_type))
|
||||
if cast:
|
||||
return cast(value)
|
||||
raise TypeError(f"Type mismatch while writing param {key}: {proposed_type=} {expected_type=} {value=}")
|
||||
|
||||
def _cpp2python(self, t, value, default, key):
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return CPP_2_PYTHON[t](value)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
cloudlog.warning(f"Failed to cast param {key} with {value=} from type {t=}")
|
||||
return self._cpp2python(t, default, None, key)
|
||||
|
||||
def get(self, key, bool block=False, bool return_default=False):
|
||||
cdef string k = self.check_key(key)
|
||||
cdef ParamKeyType t = self.p.getKeyType(k)
|
||||
cdef optional[string] default = self.p.getKeyDefaultValue(k)
|
||||
cdef string val
|
||||
with nogil:
|
||||
val = self.p.get(k, block)
|
||||
|
||||
default_val = (default.value() if default.has_value() else None) if return_default else None
|
||||
if val == b"":
|
||||
if block:
|
||||
# If we got no value while running in blocked mode
|
||||
# it means we got an interrupt while waiting
|
||||
raise KeyboardInterrupt
|
||||
else:
|
||||
return self._cpp2python(t, default_val, None, key)
|
||||
return self._cpp2python(t, val, default_val, key)
|
||||
|
||||
def get_bool(self, key, bool block=False):
|
||||
cdef string k = self.check_key(key)
|
||||
cdef bool r
|
||||
with nogil:
|
||||
r = self.p.getBool(k, block)
|
||||
return r
|
||||
|
||||
def _put_cast(self, key, dat):
|
||||
cdef string k = self.check_key(key)
|
||||
cdef ParamKeyType t = self.p.getKeyType(k)
|
||||
return ensure_bytes(self.python2cpp(type(dat), t, dat, key))
|
||||
|
||||
def put(self, key, dat, bool block = False):
|
||||
"""
|
||||
Warning: block=True blocks until the param is written to disk!
|
||||
In very rare cases this can take over a second, and your code will hang.
|
||||
Use block=False in time sensitive code, but in general try to avoid
|
||||
writing params as much as possible.
|
||||
"""
|
||||
cdef string k = self.check_key(key)
|
||||
cdef string dat_bytes = self._put_cast(key, dat)
|
||||
with nogil:
|
||||
if block:
|
||||
self.p.put(k, dat_bytes)
|
||||
else:
|
||||
self.p.putNonBlocking(k, dat_bytes)
|
||||
|
||||
def put_bool(self, key, bool val, bool block = False):
|
||||
cdef string k = self.check_key(key)
|
||||
with nogil:
|
||||
if block:
|
||||
self.p.putBool(k, val)
|
||||
else:
|
||||
self.p.putBoolNonBlocking(k, val)
|
||||
|
||||
def remove(self, key):
|
||||
cdef string k = self.check_key(key)
|
||||
with nogil:
|
||||
self.p.remove(k)
|
||||
|
||||
def get_param_path(self, key=""):
|
||||
cdef string key_bytes = ensure_bytes(key)
|
||||
return self.p.getParamPath(key_bytes).decode("utf-8")
|
||||
|
||||
def get_type(self, key):
|
||||
return self.p.getKeyType(self.check_key(key))
|
||||
|
||||
def all_keys(self, flag=ParamKeyFlag.ALL):
|
||||
return self.p.allKeys(flag)
|
||||
|
||||
def get_default_value(self, key):
|
||||
cdef string k = self.check_key(key)
|
||||
cdef ParamKeyType t = self.p.getKeyType(k)
|
||||
cdef optional[string] default = self.p.getKeyDefaultValue(k)
|
||||
return self._cpp2python(t, default.value(), None, key) if default.has_value() else None
|
||||
|
||||
def cpp2python(self, key, value):
|
||||
cdef string k = self.check_key(key)
|
||||
cdef ParamKeyType t = self.p.getKeyType(k)
|
||||
return self._cpp2python(t, value, None, key)
|
||||
@@ -0,0 +1,218 @@
|
||||
"""Small QR encoder for the UI's byte-mode, error-correction-level-L codes."""
|
||||
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
|
||||
# Indexes are QR versions. These are the only two Reed-Solomon parameters needed
|
||||
# for error-correction level L.
|
||||
_ECC_LEN = (0, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28)
|
||||
_NUM_BLOCKS = (0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8)
|
||||
|
||||
# 15 format-info bits for level L (01) with mask 0: ((0x08 << 10) | bch_remainder) ^ 0x5412
|
||||
_FORMAT_BITS = 0b111011111000100
|
||||
|
||||
|
||||
def _raw_modules(version: int) -> int:
|
||||
result = (16 * version + 128) * version + 64
|
||||
if version >= 2:
|
||||
align = version // 7 + 2
|
||||
result -= (25 * align - 10) * align - 55
|
||||
return result - (36 if version >= 7 else 0)
|
||||
|
||||
|
||||
def _capacity(version: int) -> int:
|
||||
return _raw_modules(version) // 8 - _ECC_LEN[version] * _NUM_BLOCKS[version]
|
||||
|
||||
|
||||
def _append_bits(bits: list[int], value: int, length: int) -> None:
|
||||
bits.extend((value >> i) & 1 for i in range(length - 1, -1, -1))
|
||||
|
||||
|
||||
def _data_codewords(data: bytes, version: int) -> bytes:
|
||||
"""Byte-mode-encode the payload, terminated and padded to the version's capacity."""
|
||||
capacity = _capacity(version)
|
||||
bits: list[int] = []
|
||||
_append_bits(bits, 4, 4) # byte mode
|
||||
_append_bits(bits, len(data), 8 if version <= 9 else 16)
|
||||
for value in data:
|
||||
_append_bits(bits, value, 8)
|
||||
bits.extend([0] * min(4, capacity * 8 - len(bits))) # terminator
|
||||
bits.extend([0] * (-len(bits) % 8)) # byte alignment
|
||||
result = bytearray(sum(bits[i + j] << (7 - j) for j in range(8)) for i in range(0, len(bits), 8))
|
||||
pad = (0xEC, 0x11)
|
||||
while len(result) < capacity:
|
||||
result.append(pad[(len(result) - (len(bits) // 8)) & 1])
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def _codewords(data: bytes, version: int) -> bytes:
|
||||
"""Split data codewords into Reed-Solomon blocks and interleave data + ECC."""
|
||||
data = _data_codewords(data, version)
|
||||
num_blocks = _NUM_BLOCKS[version]
|
||||
ecc_len = _ECC_LEN[version]
|
||||
raw_codewords = _raw_modules(version) // 8
|
||||
short_len = raw_codewords // num_blocks
|
||||
num_short = num_blocks - raw_codewords % num_blocks
|
||||
divisor = _divisor(ecc_len)
|
||||
blocks: list[tuple[bytes, bytes]] = []
|
||||
offset = 0
|
||||
for i in range(num_blocks):
|
||||
length = short_len - ecc_len + (0 if i < num_short else 1)
|
||||
block = data[offset:offset + length]
|
||||
blocks.append((block, _remainder(block, divisor)))
|
||||
offset += length
|
||||
result = bytearray()
|
||||
for i in range(short_len - ecc_len + 1):
|
||||
for block, _ in blocks:
|
||||
result.extend(block[i:i + 1])
|
||||
for i in range(ecc_len):
|
||||
for _, ecc in blocks:
|
||||
result.append(ecc[i])
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def _multiply(x: int, y: int) -> int:
|
||||
result = 0
|
||||
for _ in range(8):
|
||||
result = (result << 1) ^ (0x11D if result & 0x80 else 0)
|
||||
if y & 0x80:
|
||||
result ^= x
|
||||
y <<= 1
|
||||
return result
|
||||
|
||||
|
||||
def _divisor(degree: int) -> bytes:
|
||||
result = bytearray([0] * (degree - 1) + [1])
|
||||
root = 1
|
||||
for _ in range(degree):
|
||||
for j in range(degree):
|
||||
result[j] = _multiply(result[j], root)
|
||||
if j + 1 < degree:
|
||||
result[j] ^= result[j + 1]
|
||||
root = _multiply(root, 2)
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def _remainder(data: bytes, divisor: bytes) -> bytes:
|
||||
result = bytearray(len(divisor))
|
||||
for value in data:
|
||||
factor = value ^ result.pop(0)
|
||||
result.append(0)
|
||||
for i, coefficient in enumerate(divisor):
|
||||
result[i] ^= _multiply(coefficient, factor)
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def _alignment_positions(version: int) -> list[int]:
|
||||
if version == 1:
|
||||
return []
|
||||
count = version // 7 + 2
|
||||
step = ((version * 4 + count * 2 + 1) // (count * 2 - 2)) * 2
|
||||
return [6] + [version * 4 + 10 - step * i for i in range(count - 1)][::-1]
|
||||
|
||||
|
||||
class _Qr:
|
||||
def __init__(self, version: int, data: bytes):
|
||||
self.version = version
|
||||
self.size = version * 4 + 17
|
||||
self.modules = [[False] * self.size for _ in range(self.size)]
|
||||
self.function = [[False] * self.size for _ in range(self.size)]
|
||||
self._draw_functions()
|
||||
self._draw_data(_codewords(data, version))
|
||||
for y in range(self.size):
|
||||
for x in range(self.size):
|
||||
if not self.function[y][x]:
|
||||
self.modules[y][x] ^= (x + y) % 2 == 0
|
||||
self._format()
|
||||
|
||||
def _set_function(self, x: int, y: int, dark: bool) -> None:
|
||||
if 0 <= x < self.size and 0 <= y < self.size:
|
||||
self.modules[y][x] = dark
|
||||
self.function[y][x] = True
|
||||
|
||||
def _finder(self, x: int, y: int) -> None:
|
||||
for dy in range(-4, 5):
|
||||
for dx in range(-4, 5):
|
||||
distance = max(abs(dx), abs(dy))
|
||||
self._set_function(x + dx, y + dy, distance != 2 and distance != 4)
|
||||
|
||||
def _alignment(self, x: int, y: int) -> None:
|
||||
for dy in range(-2, 3):
|
||||
for dx in range(-2, 3):
|
||||
self._set_function(x + dx, y + dy, max(abs(dx), abs(dy)) != 1)
|
||||
|
||||
def _draw_functions(self) -> None:
|
||||
for i in range(self.size):
|
||||
self._set_function(6, i, i % 2 == 0)
|
||||
self._set_function(i, 6, i % 2 == 0)
|
||||
self._finder(3, 3)
|
||||
self._finder(self.size - 4, 3)
|
||||
self._finder(3, self.size - 4)
|
||||
positions = _alignment_positions(self.version)
|
||||
for y in positions:
|
||||
for x in positions:
|
||||
if not ((x == 6 and y in (6, self.size - 7)) or (x == self.size - 7 and y == 6)):
|
||||
self._alignment(x, y)
|
||||
# reserve the format-info modules before the data is placed; the real
|
||||
# values are written by the second _format call after masking
|
||||
self._format()
|
||||
if self.version >= 7:
|
||||
value = self.version
|
||||
for _ in range(12):
|
||||
value = (value << 1) ^ ((value >> 11) * 0x1F25)
|
||||
value = self.version << 12 | value
|
||||
for i in range(18):
|
||||
bit = ((value >> i) & 1) != 0
|
||||
a = self.size - 11 + i % 3
|
||||
b = i // 3
|
||||
self._set_function(a, b, bit)
|
||||
self._set_function(b, a, bit)
|
||||
|
||||
def _format(self) -> None:
|
||||
for i in range(15):
|
||||
bit = ((_FORMAT_BITS >> i) & 1) != 0
|
||||
y_pos = i if i < 6 else i + 1 if i < 8 else self.size - 15 + i
|
||||
self._set_function(8, y_pos, bit)
|
||||
x_pos = self.size - 1 - i if i < 8 else 15 - i if i < 9 else 14 - i
|
||||
self._set_function(x_pos, 8, bit)
|
||||
self._set_function(8, self.size - 8, True)
|
||||
|
||||
def _draw_data(self, data: bytes) -> None:
|
||||
bits = ((byte >> s) & 1 for byte in data for s in reversed(range(8)))
|
||||
upward = True
|
||||
right = self.size - 1
|
||||
while right >= 1:
|
||||
if right == 6: # skip the vertical timing column
|
||||
right = 5
|
||||
for vert in range(self.size):
|
||||
y = self.size - 1 - vert if upward else vert
|
||||
for x in (right, right - 1):
|
||||
if not self.function[y][x]:
|
||||
self.modules[y][x] = bool(next(bits, 0))
|
||||
upward = not upward
|
||||
right -= 2
|
||||
|
||||
|
||||
def make_texture(data: str, inverted: bool = False) -> rl.Texture:
|
||||
"""Render a URL as the RGBA QR texture used by the UI. The texture upload
|
||||
copies the pixels, so the intermediate image/array don't need to outlive it."""
|
||||
raw = data.encode()
|
||||
for version in range(1, 21):
|
||||
count_bits = 8 if version <= 9 else 16
|
||||
if 4 + count_bits + len(raw) * 8 <= _capacity(version) * 8:
|
||||
break
|
||||
else:
|
||||
raise ValueError("QR URL is too long")
|
||||
modules = np.pad(_Qr(version, raw).modules, 0 if inverted else 4)
|
||||
modules = np.repeat(np.repeat(modules, 10, axis=0), 10, axis=1)
|
||||
gray = ((modules == inverted) * 255).astype(np.uint8)
|
||||
img_array = np.dstack((gray, gray, gray, np.full_like(gray, 255)))
|
||||
|
||||
rl_image = rl.Image()
|
||||
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
|
||||
rl_image.width = img_array.shape[1]
|
||||
rl_image.height = img_array.shape[0]
|
||||
rl_image.mipmaps = 1
|
||||
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
|
||||
return rl.load_texture_from_image(rl_image)
|
||||
@@ -48,9 +48,6 @@ class RunningStat:
|
||||
def std(self):
|
||||
return np.sqrt(self.variance())
|
||||
|
||||
def params_to_save(self):
|
||||
return [self.M, self.S, self.n]
|
||||
|
||||
class RunningStatFilter:
|
||||
def __init__(self, raw_priors=None, filtered_priors=None, max_trackable=-1):
|
||||
self.raw_stat = RunningStat(raw_priors, -1)
|
||||
|
||||
@@ -45,7 +45,8 @@ class SwaglogRotatingFileHandler(BaseRotatingHandler):
|
||||
fp = os.path.join(base_dir, fn)
|
||||
if fp.startswith(self.base_filename) and os.path.isfile(fp):
|
||||
log_files.append(fp)
|
||||
return sorted(log_files)
|
||||
# newest first, matching _open()'s insert(0, ...) so doRollover()'s pop() deletes the oldest
|
||||
return sorted(log_files, reverse=True)
|
||||
|
||||
def shouldRollover(self, record):
|
||||
size_exceeded = self.max_bytes > 0 and self.stream.tell() >= self.max_bytes
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import contextlib
|
||||
import gc
|
||||
import inspect
|
||||
import os
|
||||
import subprocess
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.system.manager import manager
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def clean_env():
|
||||
starting_env = dict(os.environ)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.environ.clear()
|
||||
os.environ.update(starting_env)
|
||||
|
||||
|
||||
class OpenpilotTestCase(unittest.TestCase):
|
||||
"""TestCase with openpilot's per-test isolation."""
|
||||
|
||||
COMMA_HARDWARE_TEST = False
|
||||
SHARED_DOWNLOAD_CACHE = False
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
# Preserve legacy xunit hooks and invoke them inside the prefix below.
|
||||
for name in ("setup_method", "teardown_method"):
|
||||
hook = cls.__dict__.get(name)
|
||||
if hook is not None:
|
||||
setattr(cls, f"openpilot_{name}", hook)
|
||||
setattr(cls, name, None)
|
||||
|
||||
def _fixture(self, name):
|
||||
if name == "mocker":
|
||||
return Mocker(self.addCleanup)
|
||||
if name == "monkeypatch":
|
||||
return MonkeyPatch(self.addCleanup)
|
||||
if name == "subtests":
|
||||
return SubTests(self)
|
||||
|
||||
fixture = getattr(inspect.getmodule(type(self)), name)
|
||||
kwargs = {p: self._fixture(p) for p in inspect.signature(fixture).parameters}
|
||||
value = fixture(**kwargs)
|
||||
if inspect.isgenerator(value):
|
||||
generator = value
|
||||
value = next(generator)
|
||||
self.addCleanup(lambda: next(generator, None))
|
||||
return value
|
||||
|
||||
def _callTestMethod(self, method):
|
||||
params = [name for name, param in inspect.signature(method).parameters.items()
|
||||
if param.default is inspect.Parameter.empty]
|
||||
return method(**{name: self._fixture(name) for name in params})
|
||||
|
||||
def run(self, result=None):
|
||||
# This boundary cannot live in setUp/tearDown: existing unittest classes
|
||||
# are allowed to override those hooks without calling super().
|
||||
if (self.COMMA_HARDWARE_TEST and not COMMA_HARDWARE) or getattr(type(self), "__unittest_skip__", False):
|
||||
return super().run(result)
|
||||
test_env = clean_env()
|
||||
test_env.__enter__()
|
||||
prefix = OpenpilotPrefix(shared_download_cache=self.SHARED_DOWNLOAD_CACHE)
|
||||
prefix.__enter__()
|
||||
try:
|
||||
return super().run(result)
|
||||
finally:
|
||||
prefix.__exit__(None, None, None)
|
||||
manager.manager_cleanup()
|
||||
if not gc.isenabled():
|
||||
gc.enable()
|
||||
gc.collect()
|
||||
test_env.__exit__(None, None, None)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
if cls.COMMA_HARDWARE_TEST and not COMMA_HARDWARE:
|
||||
raise unittest.SkipTest("Skipping comma hardware test on PC")
|
||||
cls._class_env = clean_env()
|
||||
cls._class_env.__enter__()
|
||||
setup_class = getattr(cls, "setup_class", None)
|
||||
if setup_class is not None:
|
||||
setup_class()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
try:
|
||||
teardown_class = getattr(cls, "teardown_class", None)
|
||||
if teardown_class is not None:
|
||||
teardown_class()
|
||||
finally:
|
||||
cls._class_env.__exit__(None, None, None)
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
if self.COMMA_HARDWARE_TEST and not COMMA_HARDWARE:
|
||||
self.skipTest("Skipping comma hardware test on PC")
|
||||
|
||||
if self.COMMA_HARDWARE_TEST:
|
||||
HARDWARE.initialize_hardware()
|
||||
HARDWARE.set_power_save(False)
|
||||
subprocess.run(["pkill", "-9", "-f", "athena"], check=False)
|
||||
|
||||
setup_method = getattr(self, "openpilot_setup_method", None)
|
||||
if setup_method is not None:
|
||||
setup_method()
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
teardown_method = getattr(self, "openpilot_teardown_method", None)
|
||||
if teardown_method is not None:
|
||||
teardown_method()
|
||||
finally:
|
||||
super().tearDown()
|
||||
|
||||
|
||||
class Mocker:
|
||||
Mock = mock.Mock
|
||||
MagicMock = mock.MagicMock
|
||||
call = mock.call
|
||||
ANY = mock.ANY
|
||||
|
||||
def __init__(self, add_cleanup):
|
||||
self._add_cleanup = add_cleanup
|
||||
self.patch = Patch(self._start)
|
||||
|
||||
def _start(self, patcher):
|
||||
value = patcher.start()
|
||||
self._add_cleanup(patcher.stop)
|
||||
return value
|
||||
|
||||
|
||||
class Patch:
|
||||
def __init__(self, start):
|
||||
self._start = start
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self._start(mock.patch(*args, **kwargs))
|
||||
|
||||
def object(self, *args, **kwargs):
|
||||
return self._start(mock.patch.object(*args, **kwargs))
|
||||
|
||||
|
||||
class MonkeyPatch:
|
||||
def __init__(self, add_cleanup):
|
||||
self._add_cleanup = add_cleanup
|
||||
|
||||
def setattr(self, target, name, value):
|
||||
patcher = mock.patch.object(target, name, value)
|
||||
patcher.start()
|
||||
self._add_cleanup(patcher.stop)
|
||||
|
||||
|
||||
class SubTests:
|
||||
def __init__(self, test_case):
|
||||
self._test_case = test_case
|
||||
|
||||
def test(self, label=None, **kwargs):
|
||||
return self._test_case.subTest(**kwargs) if label is None else self._test_case.subTest(label, **kwargs)
|
||||
@@ -1 +1,2 @@
|
||||
test_common
|
||||
test_swaglog
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
inline void native_test_check(bool condition, const char *expression, const char *file, int line) {
|
||||
if (!condition) {
|
||||
throw std::runtime_error(std::string(file) + ":" + std::to_string(line) + ": check failed: " + expression);
|
||||
}
|
||||
}
|
||||
|
||||
#define CHECK(condition) native_test_check(static_cast<bool>(condition), #condition, __FILE__, __LINE__)
|
||||
#define REQUIRE(...) CHECK((__VA_ARGS__))
|
||||
|
||||
template <typename Function>
|
||||
int run_native_test(Function &&function) {
|
||||
try {
|
||||
function();
|
||||
return 0;
|
||||
} catch (const std::exception &error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.utils import atomic_write
|
||||
|
||||
|
||||
class TestFileHelpers:
|
||||
class TestFileHelpers(OpenpilotTestCase):
|
||||
def run_atomic_write_func(self, atomic_write_func):
|
||||
path = f"/tmp/tmp{uuid4()}"
|
||||
with atomic_write_func(path) as f:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.markdown import parse_markdown
|
||||
|
||||
|
||||
class TestMarkdown:
|
||||
class TestMarkdown(OpenpilotTestCase):
|
||||
def test_all_release_notes(self):
|
||||
with open(os.path.join(BASEDIR, "CHANGELOG.md")) as f:
|
||||
release_notes = f.read().split("\n\n")
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import pytest
|
||||
import datetime
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.params import Params, ParamKeyFlag, UnknownKeyName
|
||||
|
||||
class TestParams:
|
||||
class TestParams(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
self.params = Params()
|
||||
|
||||
@@ -50,18 +50,23 @@ class TestParams:
|
||||
assert self.params.get("CarParams", block=True) == b"test"
|
||||
|
||||
def test_params_unknown_key_fails(self):
|
||||
with pytest.raises(UnknownKeyName):
|
||||
with self.assertRaises(UnknownKeyName):
|
||||
self.params.get("swag")
|
||||
|
||||
with pytest.raises(UnknownKeyName):
|
||||
with self.assertRaises(UnknownKeyName):
|
||||
self.params.get_bool("swag")
|
||||
|
||||
with pytest.raises(UnknownKeyName):
|
||||
with self.assertRaises(UnknownKeyName):
|
||||
self.params.put("swag", "abc", block=True)
|
||||
|
||||
with pytest.raises(UnknownKeyName):
|
||||
with self.assertRaises(UnknownKeyName):
|
||||
self.params.put_bool("swag", True, block=True)
|
||||
|
||||
with self.assertRaises(UnknownKeyName):
|
||||
self.params.put(b"DongleId\0suffix", "abc", block=True)
|
||||
|
||||
assert self.params.get_param_path(b"key\0suffix").endswith("/key\0suffix")
|
||||
|
||||
def test_remove_not_there(self):
|
||||
assert self.params.get("CarParams") is None
|
||||
self.params.remove("CarParams")
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch2/catch.hpp"
|
||||
@@ -1,7 +1,8 @@
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.simple_kalman import KF1D
|
||||
|
||||
|
||||
class TestSimpleKalman:
|
||||
class TestSimpleKalman(OpenpilotTestCase):
|
||||
def setup_method(self):
|
||||
dt = 0.01
|
||||
x0_0 = 0.0
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include <zmq.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "catch2/catch.hpp"
|
||||
#include "common/swaglog.h"
|
||||
#include "common/util.h"
|
||||
#include "common/version.h"
|
||||
#include "common/hardware/hw.h"
|
||||
#include "common/swaglog.h"
|
||||
#include "common/tests/native_test.h"
|
||||
#include "json11/json11.hpp"
|
||||
|
||||
#include "sunnypilot/common/version.h"
|
||||
@@ -15,12 +14,38 @@ std::string daemon_name = "testy";
|
||||
std::string dongle_id = "test_dongle_id";
|
||||
int LINE_NO = 0;
|
||||
|
||||
void log_thread(int thread_id, int msg_cnt) {
|
||||
for (int i = 0; i < msg_cnt; ++i) {
|
||||
LOGD("%d", thread_id);
|
||||
LINE_NO = __LINE__ - 1;
|
||||
usleep(1);
|
||||
}
|
||||
void test_swaglog() {
|
||||
setenv("MANAGER_DAEMON", "swaglog_test", 1);
|
||||
setenv("DONGLE_ID", "test_dongle_id", 1);
|
||||
setenv("CLEAN", "1", 1);
|
||||
|
||||
void *context = zmq_ctx_new();
|
||||
CHECK(context != nullptr);
|
||||
void *socket = zmq_socket(context, ZMQ_PULL);
|
||||
CHECK(socket != nullptr);
|
||||
int timeout = 5000;
|
||||
CHECK(zmq_setsockopt(socket, ZMQ_RCVTIMEO, &timeout, sizeof(timeout)) == 0);
|
||||
CHECK(zmq_bind(socket, Path::swaglog_ipc().c_str()) == 0);
|
||||
|
||||
LOGD("native-cpp-log");
|
||||
|
||||
char buffer[4096] = {};
|
||||
const int size = zmq_recv(socket, buffer, sizeof(buffer), 0);
|
||||
CHECK(size > 1);
|
||||
CHECK(buffer[0] == CLOUDLOG_DEBUG);
|
||||
std::string error;
|
||||
const auto message = json11::Json::parse(std::string(buffer + 1, size - 1), error);
|
||||
CHECK(error.empty());
|
||||
CHECK(message["levelnum"].int_value() == CLOUDLOG_DEBUG);
|
||||
CHECK(message["msg"].string_value() == "native-cpp-log");
|
||||
CHECK(message["funcname"].string_value() == "test_swaglog");
|
||||
CHECK(message["filename"].string_value().find("test_swaglog.cc") != std::string::npos);
|
||||
CHECK(message["ctx"]["daemon"].string_value() == "swaglog_test");
|
||||
CHECK(message["ctx"]["dongle_id"].string_value() == "test_dongle_id");
|
||||
CHECK(message["ctx"]["dirty"].bool_value() == false);
|
||||
|
||||
CHECK(zmq_close(socket) == 0);
|
||||
CHECK(zmq_ctx_destroy(context) == 0);
|
||||
}
|
||||
|
||||
void recv_log(int thread_cnt, int thread_msg_cnt) {
|
||||
@@ -39,52 +64,39 @@ void recv_log(int thread_cnt, int thread_msg_cnt) {
|
||||
break;
|
||||
}
|
||||
|
||||
REQUIRE(buf[0] == CLOUDLOG_DEBUG);
|
||||
CHECK(buf[0] == CLOUDLOG_DEBUG);
|
||||
std::string err;
|
||||
auto msg = json11::Json::parse(buf + 1, err);
|
||||
REQUIRE(!msg.is_null());
|
||||
CHECK(!msg.is_null());
|
||||
|
||||
REQUIRE(msg["levelnum"].int_value() == CLOUDLOG_DEBUG);
|
||||
REQUIRE_THAT(msg["filename"].string_value(), Catch::Contains("test_swaglog.cc"));
|
||||
REQUIRE(msg["funcname"].string_value() == "log_thread");
|
||||
REQUIRE(msg["lineno"].int_value() == LINE_NO);
|
||||
CHECK(msg["levelnum"].int_value() == CLOUDLOG_DEBUG);
|
||||
CHECK(msg["filename"].string_value().find("test_swaglog.cc") != std::string::npos);
|
||||
CHECK(msg["funcname"].string_value() == "log_thread");
|
||||
CHECK(msg["lineno"].int_value() == LINE_NO);
|
||||
|
||||
auto ctx = msg["ctx"];
|
||||
|
||||
REQUIRE(ctx["daemon"].string_value() == daemon_name);
|
||||
REQUIRE(ctx["dongle_id"].string_value() == dongle_id);
|
||||
REQUIRE(ctx["dirty"].bool_value() == true);
|
||||
CHECK(ctx["daemon"].string_value() == daemon_name);
|
||||
CHECK(ctx["dongle_id"].string_value() == dongle_id);
|
||||
CHECK(ctx["dirty"].bool_value() == true);
|
||||
|
||||
REQUIRE(ctx["version"].string_value() == SUNNYPILOT_VERSION);
|
||||
CHECK(ctx["version"].string_value() == SUNNYPILOT_VERSION);
|
||||
|
||||
std::string device = Hardware::get_name();
|
||||
REQUIRE(ctx["device"].string_value() == device);
|
||||
CHECK(ctx["device"].string_value() == device);
|
||||
|
||||
int thread_id = atoi(msg["msg"].string_value().c_str());
|
||||
REQUIRE((thread_id >= 0 && thread_id < thread_cnt));
|
||||
CHECK((thread_id >= 0 && thread_id < thread_cnt));
|
||||
thread_msgs[thread_id]++;
|
||||
total_count++;
|
||||
}
|
||||
for (int i = 0; i < thread_cnt; ++i) {
|
||||
INFO("thread :" << i);
|
||||
REQUIRE(thread_msgs[i] == thread_msg_cnt);
|
||||
CHECK(thread_msgs[i] == thread_msg_cnt);
|
||||
}
|
||||
zmq_close(sock);
|
||||
zmq_ctx_destroy(zctx);
|
||||
}
|
||||
|
||||
TEST_CASE("swaglog") {
|
||||
setenv("MANAGER_DAEMON", daemon_name.c_str(), 1);
|
||||
setenv("DONGLE_ID", dongle_id.c_str(), 1);
|
||||
setenv("dirty", "1", 1);
|
||||
const int thread_cnt = 5;
|
||||
const int thread_msg_cnt = 100;
|
||||
|
||||
std::vector<std::thread> log_threads;
|
||||
for (int i = 0; i < thread_cnt; ++i) {
|
||||
log_threads.push_back(std::thread(log_thread, i, thread_msg_cnt));
|
||||
}
|
||||
for (auto &t : log_threads) t.join();
|
||||
|
||||
recv_log(thread_cnt, thread_msg_cnt);
|
||||
int main() {
|
||||
return run_native_test(test_swaglog);
|
||||
}
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
#include <string>
|
||||
|
||||
#include "catch2/catch.hpp"
|
||||
#include "common/util.h"
|
||||
|
||||
std::string random_bytes(int size) {
|
||||
std::random_device rd;
|
||||
std::independent_bits_engine<std::default_random_engine, CHAR_BIT, unsigned char> rbe(rd());
|
||||
std::string bytes(size + 1, '\0');
|
||||
std::generate(bytes.begin(), bytes.end(), std::ref(rbe));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
TEST_CASE("util::read_file") {
|
||||
SECTION("read /proc/version") {
|
||||
std::string ret = util::read_file("/proc/version");
|
||||
REQUIRE(ret.find("Linux version") != std::string::npos);
|
||||
}
|
||||
SECTION("read from sysfs") {
|
||||
std::string ret = util::read_file("/sys/power/wakeup_count");
|
||||
REQUIRE(!ret.empty());
|
||||
}
|
||||
SECTION("read file") {
|
||||
char filename[] = "/tmp/test_read_XXXXXX";
|
||||
int fd = mkstemp(filename);
|
||||
|
||||
REQUIRE(util::read_file(filename).empty());
|
||||
|
||||
std::string content = random_bytes(64 * 1024);
|
||||
REQUIRE(write(fd, content.c_str(), content.size()) == (ssize_t)content.size());
|
||||
std::string ret = util::read_file(filename);
|
||||
bool equal = (ret == content);
|
||||
REQUIRE(equal);
|
||||
close(fd);
|
||||
}
|
||||
SECTION("read directory") {
|
||||
REQUIRE(util::read_file(".").empty());
|
||||
}
|
||||
SECTION("read non-existent file") {
|
||||
std::string ret = util::read_file("does_not_exist");
|
||||
REQUIRE(ret.empty());
|
||||
}
|
||||
SECTION("read non-permission") {
|
||||
REQUIRE(util::read_file("/proc/kmsg").empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("util::file_exists") {
|
||||
char filename[] = "/tmp/test_file_exists_XXXXXX";
|
||||
int fd = mkstemp(filename);
|
||||
REQUIRE(fd != -1);
|
||||
close(fd);
|
||||
|
||||
SECTION("existent file") {
|
||||
REQUIRE(util::file_exists(filename));
|
||||
REQUIRE(util::file_exists("/tmp"));
|
||||
}
|
||||
SECTION("nonexistent file") {
|
||||
std::string fn = filename;
|
||||
REQUIRE(!util::file_exists(fn + "/nonexistent"));
|
||||
}
|
||||
SECTION("file has no access permissions") {
|
||||
std::string fn = "/proc/kmsg";
|
||||
std::ifstream f(fn);
|
||||
REQUIRE(f.good() == false);
|
||||
REQUIRE(util::file_exists(fn));
|
||||
}
|
||||
::remove(filename);
|
||||
}
|
||||
|
||||
TEST_CASE("util::read_files_in_dir") {
|
||||
char tmp_path[] = "/tmp/test_XXXXXX";
|
||||
const std::string test_path = mkdtemp(tmp_path);
|
||||
const std::string files[] = {".test1", "'test2'", "test3"};
|
||||
for (auto fn : files) {
|
||||
std::ofstream{test_path + "/" + fn} << fn;
|
||||
}
|
||||
mkdir((test_path + "/dir").c_str(), 0777);
|
||||
|
||||
std::map<std::string, std::string> result = util::read_files_in_dir(test_path);
|
||||
REQUIRE(result.find("dir") == result.end());
|
||||
REQUIRE(result.size() == std::size(files));
|
||||
for (auto& [k, v] : result) {
|
||||
REQUIRE(k == v);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("util::safe_fwrite") {
|
||||
char filename[] = "/tmp/XXXXXX";
|
||||
int fd = mkstemp(filename);
|
||||
close(fd);
|
||||
std::string dat = random_bytes(1024 * 1024);
|
||||
|
||||
FILE *f = util::safe_fopen(filename, "wb");
|
||||
REQUIRE(f != nullptr);
|
||||
size_t size = util::safe_fwrite(dat.data(), 1, dat.size(), f);
|
||||
REQUIRE(size == dat.size());
|
||||
int ret = util::safe_fflush(f);
|
||||
REQUIRE(ret == 0);
|
||||
ret = fclose(f);
|
||||
REQUIRE(ret == 0);
|
||||
bool equal = (dat == util::read_file(filename));
|
||||
REQUIRE(equal);
|
||||
}
|
||||
|
||||
TEST_CASE("util::create_directories") {
|
||||
REQUIRE(system("rm /tmp/test_create_directories -rf") == 0);
|
||||
std::string dir = "/tmp/test_create_directories/a/b/c/d/e/f";
|
||||
|
||||
auto check_dir_permissions = [](const std::string &path, mode_t mode) -> bool {
|
||||
struct stat st = {};
|
||||
return stat(path.c_str(), &st) == 0 && (st.st_mode & S_IFMT) == S_IFDIR && (st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) == mode;
|
||||
};
|
||||
|
||||
SECTION("create_directories") {
|
||||
REQUIRE(util::create_directories(dir, 0755));
|
||||
REQUIRE(check_dir_permissions(dir, 0755));
|
||||
}
|
||||
SECTION("dir already exists") {
|
||||
REQUIRE(util::create_directories(dir, 0755));
|
||||
REQUIRE(util::create_directories(dir, 0755));
|
||||
}
|
||||
SECTION("a file exists with the same name") {
|
||||
REQUIRE(util::create_directories(dir, 0755));
|
||||
int f = open((dir + "/file").c_str(), O_RDWR | O_CREAT, 0644);
|
||||
REQUIRE(f != -1);
|
||||
close(f);
|
||||
REQUIRE(util::create_directories(dir + "/file", 0755) == false);
|
||||
REQUIRE(util::create_directories(dir + "/file/1/2/3", 0755) == false);
|
||||
}
|
||||
SECTION("end with slashes") {
|
||||
REQUIRE(util::create_directories(dir + "/", 0755));
|
||||
}
|
||||
SECTION("empty") {
|
||||
REQUIRE(util::create_directories("", 0755) == false);
|
||||
}
|
||||
}
|
||||
@@ -37,12 +37,12 @@ class _NoneCameraConfig(CameraConfig):
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeviceCameraConfig:
|
||||
fcam: CameraConfig
|
||||
dcam: CameraConfig
|
||||
ecam: CameraConfig
|
||||
narrow_road: CameraConfig
|
||||
cabin: CameraConfig
|
||||
wide_road: CameraConfig
|
||||
|
||||
def all_cams(self):
|
||||
for cam in ['fcam', 'dcam', 'ecam']:
|
||||
for cam in ['narrow_road', 'cabin', 'wide_road']:
|
||||
if not isinstance(getattr(self, cam), _NoneCameraConfig):
|
||||
yield cam, getattr(self, cam)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
import openpilot.common.transformations.coordinates as coord
|
||||
|
||||
geodetic_positions = np.array([[37.7610403, -122.4778699, 115],
|
||||
@@ -41,7 +42,7 @@ ned_offsets_batch = np.array([[ 53.88103168, 43.83445935, -46.27488057],
|
||||
[ 78.56272609, 18.53100158, -43.25290759]])
|
||||
|
||||
|
||||
class TestNED:
|
||||
class TestNED(OpenpilotTestCase):
|
||||
def test_small_distances(self):
|
||||
start_geodetic = np.array([33.8042184, -117.888593, 0.0])
|
||||
local_coord = coord.LocalCoord.from_geodetic(start_geodetic)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.common.transformations.orientation import euler2quat, quat2euler, euler2rot, rot2euler, \
|
||||
rot2quat, quat2rot, \
|
||||
ned_euler_from_ecef
|
||||
@@ -30,7 +30,7 @@ ned_eulers = np.array([[ 0.46806039, -0.4881889 , 1.65697808],
|
||||
[ 2.50450101, 0.36304151, 0.33136365]])
|
||||
|
||||
|
||||
class TestOrientation:
|
||||
class TestOrientation(OpenpilotTestCase):
|
||||
def test_quat_euler(self):
|
||||
for i, eul in enumerate(eulers):
|
||||
np.testing.assert_allclose(quats[i], euler2quat(eul), rtol=1e-7)
|
||||
@@ -62,13 +62,13 @@ class TestOrientation:
|
||||
# np.testing.assert_allclose(ned_eulers, ned_euler_from_ecef(ecef_positions, eulers), rtol=1e-7)
|
||||
|
||||
def test_inputs(self):
|
||||
with pytest.raises(ValueError):
|
||||
with self.assertRaises(ValueError):
|
||||
euler2quat([1, 2])
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with self.assertRaises(ValueError):
|
||||
quat2rot([1, 2, 3])
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
with self.assertRaises(IndexError):
|
||||
rot2quat(np.zeros((2, 2)))
|
||||
|
||||
def test_euler_rot_consistency(self):
|
||||
|
||||
@@ -108,10 +108,14 @@ def atomic_write(path: str, mode: str = 'w', buffering: int = -1, encoding: str
|
||||
if not overwrite and os.path.exists(path):
|
||||
raise FileExistsError(f"File '{path}' already exists. To overwrite it, set 'overwrite' to True.")
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode=mode, buffering=buffering, encoding=encoding, newline=newline, dir=dir_name, delete=False) as tmp_file:
|
||||
yield tmp_file
|
||||
tmp_file_name = tmp_file.name
|
||||
os.replace(tmp_file_name, path)
|
||||
tmp_file = tempfile.NamedTemporaryFile(mode=mode, buffering=buffering, encoding=encoding, newline=newline, dir=dir_name, delete=False)
|
||||
try:
|
||||
with tmp_file:
|
||||
yield tmp_file
|
||||
os.replace(tmp_file.name, path)
|
||||
finally:
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
os.unlink(tmp_file.name)
|
||||
|
||||
|
||||
def get_upload_stream(filepath: str, should_compress: bool) -> tuple[io.BufferedIOBase, int]:
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:93cdc4ee9aa40e2afceecc63da0ca05ec7aab4bec991ece51a6b52389f48a477
|
||||
size 10788068
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:80135e7ab71496c3b744047ef9100f5d24edaa275d248a3e0488bc769e453baa
|
||||
size 240500
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e86fb33e127558b845aec3016d46c36118811a1e30fd9dacc374840ad3001221
|
||||
size 109356
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:71933d4233b691fb499524cbf9b3d74ccdf6465c81791687e661667aefa8e660
|
||||
size 196068
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:292f8ad096fd0a47e4016bd3607cb4edcd3b1f97634f67a115c0b0973ba5002f
|
||||
size 245884
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8656eb9e4e182333e58c59cd07ae0dd82c913d31c29b0d2726887ffbbef137a2
|
||||
size 26536
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
import pyray as rl
|
||||
|
||||
FONT_DIR = Path(__file__).resolve().parent
|
||||
SELFDRIVE_DIR = FONT_DIR.parents[1]
|
||||
TRANSLATIONS_DIR = SELFDRIVE_DIR / "ui" / "translations"
|
||||
LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json"
|
||||
|
||||
GLYPH_PADDING = 6
|
||||
EXTRA_CHARS = "–‑✓×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ–‑✓×°§•€£¥"
|
||||
UNIFONT_LANGUAGES = {"th", "zh-CHT", "zh-CHS", "ko", "ja"}
|
||||
|
||||
|
||||
def _languages():
|
||||
if not LANGUAGES_FILE.exists():
|
||||
return {}
|
||||
with LANGUAGES_FILE.open(encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _char_sets():
|
||||
base = set(map(chr, range(32, 127))) | set(EXTRA_CHARS)
|
||||
unifont = set(base)
|
||||
|
||||
for language, code in _languages().items():
|
||||
unifont.update(language)
|
||||
po_path = TRANSLATIONS_DIR / f"app_{code}.po"
|
||||
try:
|
||||
chars = set(po_path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
(unifont if code in UNIFONT_LANGUAGES else base).update(chars)
|
||||
|
||||
return tuple(sorted(ord(c) for c in base)), tuple(sorted(ord(c) for c in unifont))
|
||||
|
||||
|
||||
def _glyph_metrics(glyphs, rects, glyph_count: int):
|
||||
entries = []
|
||||
min_offset_y, max_extent = None, 0
|
||||
for idx in range(glyph_count):
|
||||
glyph = glyphs[idx]
|
||||
rect = rects[idx]
|
||||
width = int(round(rect.width))
|
||||
height = int(round(rect.height))
|
||||
offset_y = int(round(glyph.offsetY))
|
||||
min_offset_y = offset_y if min_offset_y is None else min(min_offset_y, offset_y)
|
||||
max_extent = max(max_extent, offset_y + height)
|
||||
entries.append({
|
||||
"id": glyph.value,
|
||||
"x": int(round(rect.x)),
|
||||
"y": int(round(rect.y)),
|
||||
"width": width,
|
||||
"height": height,
|
||||
"xoffset": int(round(glyph.offsetX)),
|
||||
"yoffset": offset_y,
|
||||
"xadvance": int(round(glyph.advanceX)),
|
||||
})
|
||||
|
||||
if min_offset_y is None:
|
||||
raise RuntimeError("No glyphs were generated")
|
||||
|
||||
line_height = int(round(max_extent - min_offset_y))
|
||||
base = int(round(max_extent))
|
||||
return entries, line_height, base
|
||||
|
||||
|
||||
def _write_bmfont(path: Path, font_size: int, face: str, atlas_name: str, line_height: int, base: int, atlas_size, entries):
|
||||
# TODO: why doesn't raylib calculate these metrics correctly?
|
||||
if line_height != font_size:
|
||||
print("using font size for line height", atlas_name)
|
||||
line_height = font_size
|
||||
lines = [
|
||||
f"info face=\"{face}\" size=-{font_size} bold=0 italic=0 charset=\"\" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=0,0 outline=0",
|
||||
f"common lineHeight={line_height} base={base} scaleW={atlas_size[0]} scaleH={atlas_size[1]} pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4",
|
||||
f"page id=0 file=\"{atlas_name}\"",
|
||||
f"chars count={len(entries)}",
|
||||
]
|
||||
for entry in entries:
|
||||
lines.append(
|
||||
("char id={id:<4} x={x:<5} y={y:<5} width={width:<5} height={height:<5} " +
|
||||
"xoffset={xoffset:<5} yoffset={yoffset:<5} xadvance={xadvance:<5} page=0 chnl=15").format(**entry)
|
||||
)
|
||||
path.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def _process_font(font_path: Path, codepoints: tuple[int, ...]):
|
||||
print(f"Processing {font_path.name}...")
|
||||
|
||||
font_size = {
|
||||
"unifont.otf": 16, # unifont is only 16x8 or 16x16 pixels per glyph
|
||||
}.get(font_path.name, 200)
|
||||
|
||||
data = font_path.read_bytes()
|
||||
file_buf = rl.ffi.new("unsigned char[]", data)
|
||||
cp_buffer = rl.ffi.new("int[]", codepoints)
|
||||
cp_ptr = rl.ffi.cast("int *", cp_buffer)
|
||||
glyph_count = rl.ffi.new("int *", len(codepoints))
|
||||
glyphs = rl.load_font_data(
|
||||
rl.ffi.cast("unsigned char *", file_buf), len(data), font_size, cp_ptr, len(codepoints),
|
||||
rl.FontType.FONT_DEFAULT, glyph_count
|
||||
)
|
||||
if glyphs == rl.ffi.NULL:
|
||||
raise RuntimeError("raylib failed to load font data")
|
||||
|
||||
rects_ptr = rl.ffi.new("Rectangle **")
|
||||
image = rl.gen_image_font_atlas(glyphs, rects_ptr, glyph_count[0], font_size, GLYPH_PADDING, 0)
|
||||
if image.width == 0 or image.height == 0:
|
||||
raise RuntimeError("raylib returned an empty atlas")
|
||||
|
||||
rects = rects_ptr[0]
|
||||
atlas_name = f"{font_path.stem}.png"
|
||||
atlas_path = FONT_DIR / atlas_name
|
||||
entries, line_height, base = _glyph_metrics(glyphs, rects, glyph_count[0])
|
||||
|
||||
if not rl.export_image(image, atlas_path.as_posix()):
|
||||
raise RuntimeError("Failed to export atlas image")
|
||||
|
||||
_write_bmfont(FONT_DIR / f"{font_path.stem}.fnt", font_size, font_path.stem, atlas_name, line_height, base, (image.width, image.height), entries)
|
||||
|
||||
|
||||
def main():
|
||||
base_cp, unifont_cp = _char_sets()
|
||||
fonts = sorted(FONT_DIR.glob("*.ttf")) + sorted(FONT_DIR.glob("*.otf"))
|
||||
for font in fonts:
|
||||
if "emoji" in font.name.lower():
|
||||
continue
|
||||
glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp
|
||||
_process_font(font, glyphs)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4ae6b5f666de71c8af94e8dcace3c4ca0259070cb6e8b2bbb9ba648c28a80e1
|
||||
size 5926
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a3f14ff386f5e3e8e01ac64231b77154729516aa96a8baf85fdd98fa6be20839
|
||||
size 2341
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ec3dcf64cbc34251d8423cb8b3b31d743e93d14002dec43c389a857cb7e8eb17
|
||||
size 10875
|
||||
oid sha256:d05f85ba6e53417260335bb52e2f4c492311a1eb4136e7fa1fc49aa14095bcaf
|
||||
size 1914
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8a8c5fece2a1c7587feb41cbe04c6aee08e768ecd9b5d00da6af9832a4ccc842
|
||||
size 2034
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:336230790162965fb0b1c88551c4715fe6adea0d7b39940205d7f7b871865ed7
|
||||
size 10911
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:58bd6155433f623b1f75d134bd8ca4745d9aa71f6767eb807cdbcf7deb3089a1
|
||||
size 10876
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user