diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 2bd1ccfd62..af67bdbe5d 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,6 +1,6 @@
FROM ghcr.io/commaai/openpilot-base:latest
-RUN apt update && apt install -y vim net-tools usbutils htop ripgrep tmux wget mesa-utils xvfb libxtst6 libxv1 libglu1-mesa libegl1-mesa gdb
+RUN apt update && apt install -y vim net-tools usbutils htop ripgrep tmux wget mesa-utils xvfb libxtst6 libxv1 libglu1-mesa libegl1-mesa gdb bash-completion
RUN pip install ipython jupyter jupyterlab
RUN cd /tmp && \
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 7174ee2800..3f5b38d12e 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -14,6 +14,7 @@
"force_color_prompt": "1"
},
"runArgs": [
+ "--volume=/dev:/dev",
"--volume=/tmp/.X11-unix:/tmp/.X11-unix",
"--volume=${localWorkspaceFolder}/.devcontainer/.host/.Xauthority:/home/batman/.Xauthority",
"--volume=${localEnv:HOME}/.comma:/home/batman/.comma",
@@ -49,4 +50,4 @@
"mounts": [
"type=volume,source=scons_cache,target=/tmp/scons_cache"
]
-}
\ No newline at end of file
+}
diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml
index 8316fedda0..447e559c90 100644
--- a/.github/workflows/auto_pr_review.yaml
+++ b/.github/workflows/auto_pr_review.yaml
@@ -5,7 +5,7 @@ on:
jobs:
labeler:
- name: apply labels
+ name: review
permissions:
contents: read
pull-requests: write
@@ -14,17 +14,17 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: false
+
+ # Label PRs
- uses: actions/labeler@v5.0.0
with:
dot: true
configuration-path: .github/labeler.yaml
- pr_branch_check:
- name: check branch
- runs-on: ubuntu-latest
- if: github.repository == 'commaai/openpilot'
- steps:
- - uses: Vankka/pr-target-branch-action@def32ec9d93514138d6ac0132ee62e120a72aed5
+ # Check PR target branch
+ - name: check branch
+ uses: Vankka/pr-target-branch-action@def32ec9d93514138d6ac0132ee62e120a72aed5
+ if: github.repository == 'commaai/openpilot'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -34,194 +34,20 @@ jobs:
already-exists-action: close_this
already-exists-comment: "Your PR should be made against the `master` branch"
- comment:
- runs-on: ubuntu-latest
- steps:
- - name: comment
- uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6
- if: github.event.pull_request.head.repo.full_name != 'commaai/openpilot'
- with:
- message: |
-
- Thanks for contributing to openpilot! In order for us to review your PR as quickly as possible, check the following:
- * Convert your PR to a draft unless it's ready to review
- * Read the [contributing docs](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md)
- * Before marking as "ready for review", ensure:
- * the goal is clearly stated in the description
- * all the tests are passing
- * the change is [something we merge](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md#what-gets-merged)
- * include a route or your device' dongle ID if relevant
- comment_tag: run_id
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- check-pr-template:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- issues: write
- pull-requests: write
- actions: read
- if: false && github.event.pull_request.head.repo.full_name != 'commaai/openpilot'
- steps:
- - uses: actions/github-script@v7
- with:
- script: |
- // Comment to add to the PR if no template has been used
- const NO_TEMPLATE_MESSAGE =
- "It looks like you didn't use one of the Pull Request templates. Please check [the contributing docs](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md). \
- Also make sure that you didn't modify any of the checkboxes or headings within the template.";
- // body data for future requests
- const body_data = {
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- };
-
- // Utility function to extract all headings
- const extractHeadings = (markdown) => {
- const headingRegex = /^(#{1,6})\s+(.+)$/gm;
- const boldTextRegex = /^(?:\*\*|__)(.+?)(?:\*\*|__)\s*$/gm;
- const headings = [];
- let headingMatch;
- while ((headingMatch = headingRegex.exec(markdown))) {
- headings.push(headingMatch[2].trim());
- }
- let boldMatch;
- while ((boldMatch = boldTextRegex.exec(markdown))) {
- headings.push(boldMatch[1].trim());
- }
- return headings;
- };
-
- // Utility function to extract all check box descriptions
- const extractCheckBoxTexts = (markdown) => {
- const checkboxRegex = /^\s*-\s*\[( |x)\]\s+(.+)$/gm;
- const checkboxes = [];
- let match;
- while ((match = checkboxRegex.exec(markdown))) {
- checkboxes.push(match[2].trim());
- }
- return checkboxes;
- };
-
- // Utility function to check if a list is a subset of another list
- isSubset = (subset, superset) => {
- return subset.every((item) => superset.includes(item));
- };
-
- // Utility function to check if a list of checkboxes is a subset of another list of checkboxes
- isCheckboxSubset = (templateCheckBoxTexts, prTextCheckBoxTexts) => {
- // Check if each template checkbox text is a substring of at least one PR checkbox text
- // (user should be allowed to add additional text)
- return templateCheckBoxTexts.every((item) => prTextCheckBoxTexts.some((element) => element.includes(item)))
- }
-
- // Get filenames of all currently checked-in PR templates
- const template_contents = await github.rest.repos.getContent({
- owner: context.repo.owner,
- repo: context.repo.repo,
- path: ".github/PULL_REQUEST_TEMPLATE",
- });
- var template_filenames = [];
- for (const content of template_contents.data) {
- template_filenames.push(content.path);
- }
- console.debug("Received template filenames: " + template_filenames);
- // Retrieve templates
- var templates = [];
- for (const template_filename of template_filenames) {
- const template_response = await github.rest.repos.getContent({
- owner: context.repo.owner,
- repo: context.repo.repo,
- path: template_filename,
- });
- // Convert Base64 content back
- const decoded_template = atob(template_response.data.content);
- const headings = extractHeadings(decoded_template);
- const checkboxes = extractCheckBoxTexts(decoded_template);
- if (!headings.length && !checkboxes.length) {
- console.warn(
- "Invalid template! Contains neither headings nor checkboxes, ignoring it: \n" +
- decoded_template
- );
- } else {
- templates.push({ headings: headings, checkboxes: checkboxes });
- }
- }
- // Retrieve the PR Body
- const pull_request = await github.rest.issues.get({
- ...body_data,
- });
- const pull_request_text = pull_request.data.body;
- console.debug("Received Pull Request body: \n" + pull_request_text);
-
- /* Check if the PR Body matches one of the templates
- A template is defined by all headings and checkboxes it contains
- We extract all Headings and Checkboxes from the PR text and check if any of the templates is a subset of that
- */
- const pr_headings = extractHeadings(pull_request_text);
- const pr_checkboxes = extractCheckBoxTexts(pull_request_text);
- console.debug("Found Headings in PR body:\n" + pr_headings);
- console.debug("Found Checkboxes in PR body:\n" + pr_checkboxes);
- var template_found = false;
- // Iterate over each template to check if it applies
- for (const template of templates) {
- console.log(
- "Checking for headings: [" +
- template.headings +
- "] and checkboxes: [" +
- template.checkboxes + "]"
- );
- if (
- isCheckboxSubset(template.checkboxes, pr_checkboxes) &&
- isSubset(template.headings, pr_headings)
- ) {
- console.debug("Found matching template!");
- template_found = true;
- }
- }
-
- // List comments from previous runs
- var existing_comments = [];
- const comments = await github.rest.issues.listComments({
- ...body_data,
- });
- for (const comment of comments.data) {
- if (comment.body === NO_TEMPLATE_MESSAGE) {
- existing_comments.push(comment);
- }
- }
-
- // Add a comment to the PR that it is not using a the template (but only if this comment does not exist already)
- if (!template_found) {
- var comment_already_sent = false;
-
- // Add an 'in-bot-review' label since this PR doesn't have the template
- github.rest.issues.addLabels({
- ...body_data,
- labels: ["in-bot-review"],
- });
-
- if (existing_comments.length < 1) {
- github.rest.issues.createComment({
- ...body_data,
- body: NO_TEMPLATE_MESSAGE,
- });
- }
- } else {
- // If template has been found, delete any old comment about missing template
- for (const existing_comment of existing_comments) {
- github.rest.issues.deleteComment({
- ...body_data,
- comment_id: existing_comment.id,
- });
- }
- // Remove the 'in-bot-review' label after the review is done and the PR has passed
- github.rest.issues.removeLabel({
- ...body_data,
- name: "in-bot-review",
- }).catch((error) => {
- console.log("Label 'in-bot-review' not found, ignoring");
- });
- }
-
+ # Welcome comment
+ - name: comment
+ uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6
+ if: github.event.pull_request.head.repo.full_name != 'commaai/openpilot'
+ with:
+ message: |
+
+ Thanks for contributing to openpilot! In order for us to review your PR as quickly as possible, check the following:
+ * Convert your PR to a draft unless it's ready to review
+ * Read the [contributing docs](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md)
+ * Before marking as "ready for review", ensure:
+ * the goal is clearly stated in the description
+ * all the tests are passing
+ * the change is [something we merge](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md#what-gets-merged)
+ * include a route or your device' dongle ID if relevant
+ comment_tag: run_id
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml
index 2be83b1654..9001ca86fc 100644
--- a/.github/workflows/selfdrive_tests.yaml
+++ b/.github/workflows/selfdrive_tests.yaml
@@ -47,13 +47,13 @@ jobs:
timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache
run: |
cd $STRIPPED_DIR
- ${{ env.RUN }} "python selfdrive/manager/build.py"
+ ${{ env.RUN }} "python system/manager/build.py"
- name: Run tests
timeout-minutes: 3
run: |
cd $STRIPPED_DIR
${{ env.RUN }} "release/check-dirty.sh && \
- MAX_EXAMPLES=5 $PYTEST selfdrive/car"
+ MAX_EXAMPLES=5 $PYTEST -m 'not slow' selfdrive/car"
- name: pre-commit
timeout-minutes: 3
run: |
@@ -62,7 +62,7 @@ jobs:
cp pyproject.toml $STRIPPED_DIR
cp poetry.lock $STRIPPED_DIR
cd $STRIPPED_DIR
- ${{ env.RUN }} "unset PYTHONWARNINGS && SKIP=check-added-large-files pre-commit run --all && chmod -R 777 /tmp/pre-commit"
+ ${{ env.RUN }} "unset PYTHONWARNINGS && SKIP=check-added-large-files,check-hooks-apply,check-useless-excludes pre-commit run --all && chmod -R 777 /tmp/pre-commit"
build:
strategy:
@@ -76,24 +76,8 @@ jobs:
- uses: actions/checkout@v4
with:
submodules: true
- - uses: ./.github/workflows/setup-with-retry
- with:
- docker_hub_pat: ${{ secrets.DOCKER_HUB_PAT }}
- - uses: ./.github/workflows/compile-openpilot
- timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 15 || 30) }} # allow more time when we missed the scons cache
-
- docker_push:
- name: docker push
- strategy:
- matrix:
- arch: ${{ fromJson( (github.repository == 'commaai/openpilot') && '["x86_64", "aarch64"]' || '["x86_64"]' ) }}
- runs-on: ${{ (matrix.arch == 'aarch64') && 'namespace-profile-arm64-2x8' || 'ubuntu-latest' }}
- if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot'
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: true
- - name: Setup to push to repo
+ - name: Setup docker push
+ if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot'
run: |
echo "PUSH_IMAGE=true" >> "$GITHUB_ENV"
echo "TARGET_ARCHITECTURE=${{ matrix.arch }}" >> "$GITHUB_ENV"
@@ -101,12 +85,14 @@ jobs:
- uses: ./.github/workflows/setup-with-retry
with:
docker_hub_pat: ${{ secrets.DOCKER_HUB_PAT }}
+ - uses: ./.github/workflows/compile-openpilot
+ timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 15 || 30) }} # allow more time when we missed the scons cache
docker_push_multiarch:
name: docker push multiarch tag
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot'
- needs: [docker_push]
+ needs: [build]
steps:
- uses: actions/checkout@v4
with:
@@ -134,24 +120,6 @@ jobs:
timeout-minutes: 4
run: ${{ env.RUN }} "unset PYTHONWARNINGS && pre-commit run --all && chmod -R 777 /tmp/pre-commit"
- valgrind:
- name: valgrind
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: true
- - uses: ./.github/workflows/setup-with-retry
- - name: Build openpilot
- run: ${{ env.RUN }} "scons -j$(nproc)"
- - name: Run valgrind
- timeout-minutes: 1
- run: |
- ${{ env.RUN }} "python selfdrive/test/test_valgrind_replay.py"
- - name: Print logs
- if: always()
- run: cat selfdrive/test/valgrind_logs.txt
-
unit_tests:
name: unit tests
runs-on: ${{ ((github.repository == 'commaai/openpilot') &&
@@ -200,7 +168,7 @@ jobs:
uses: actions/cache@v4
with:
path: .ci_cache/comma_download_cache
- key: proc-replay-${{ hashFiles('.github/workflows/selfdrive_tests.yaml', 'selfdrive/test/process_replay/ref_commit') }}
+ key: proc-replay-${{ hashFiles('.github/workflows/selfdrive_tests.yaml', 'selfdrive/test/process_replay/ref_commit', 'selfdrive/test/process_replay/test_regen.py') }}
- name: Build openpilot
run: |
${{ env.RUN }} "scons -j$(nproc)"
@@ -225,64 +193,18 @@ jobs:
if: ${{ failure() && steps.print-diff.outcome == 'success' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }}
run: |
${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
- - name: "Upload coverage to Codecov"
- uses: codecov/codecov-action@v4
- with:
- name: ${{ github.job }}
- env:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-
- regen:
- name: regen
- runs-on: 'ubuntu-latest'
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: true
- - uses: ./.github/workflows/setup-with-retry
- - name: Cache test routes
- id: dependency-cache
- uses: actions/cache@v4
- with:
- path: .ci_cache/comma_download_cache
- key: regen-${{ hashFiles('.github/workflows/selfdrive_tests.yaml', 'selfdrive/test/process_replay/test_regen.py') }}
- - name: Build base Docker image
- run: eval "$BUILD"
- - name: Build openpilot
- run: |
- ${{ env.RUN }} "scons -j$(nproc)"
- - name: Run regen
- timeout-minutes: 30
- run: |
- ${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \
- chmod -R 777 /tmp/comma_download_cache"
-
- test_modeld:
- name: model tests
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: true
- - uses: ./.github/workflows/setup-with-retry
- - name: Build base Docker image
- run: eval "$BUILD"
- - name: Build openpilot
- run: |
- ${{ env.RUN }} "scons -j$(nproc)"
# PYTHONWARNINGS triggers a SyntaxError in onnxruntime
- name: Run model replay with ONNX
timeout-minutes: 4
run: |
${{ env.RUN }} "unset PYTHONWARNINGS && \
- ONNXCPU=1 NO_NAV=1 coverage run selfdrive/test/process_replay/model_replay.py && \
- coverage combine && \
- coverage xml"
- - name: Run unit tests
+ ONNXCPU=1 NO_NAV=1 coverage run selfdrive/test/process_replay/model_replay.py && \
+ coverage combine && coverage xml"
+ - name: Run regen
timeout-minutes: 4
run: |
- ${{ env.RUN }} "unset PYTHONWARNINGS && \
- $PYTEST selfdrive/modeld"
+ ${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \
+ chmod -R 777 /tmp/comma_download_cache"
- name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v4
with:
@@ -298,7 +220,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- job: [0, 1, 2, 3, 4]
+ job: [0, 1]
steps:
- uses: actions/checkout@v4
with:
@@ -313,12 +235,12 @@ jobs:
- name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Test car models
- timeout-minutes: 10
+ timeout-minutes: 20
run: |
${{ env.RUN }} "$PYTEST selfdrive/car/tests/test_models.py && \
chmod -R 777 /tmp/comma_download_cache"
env:
- NUM_JOBS: 5
+ NUM_JOBS: 2
JOB_ID: ${{ matrix.job }}
- name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v4
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
index b33945cae2..3c86b8e854 100644
--- a/.github/workflows/stale.yaml
+++ b/.github/workflows/stale.yaml
@@ -5,8 +5,8 @@ on:
workflow_dispatch:
env:
- DAYS_BEFORE_PR_CLOSE: 7
- DAYS_BEFORE_PR_STALE: 30
+ DAYS_BEFORE_PR_CLOSE: 3
+ DAYS_BEFORE_PR_STALE: 14
jobs:
stale:
diff --git a/.github/workflows/tools_tests.yaml b/.github/workflows/tools_tests.yaml
index ccd3368cb0..b3cfd56d13 100644
--- a/.github/workflows/tools_tests.yaml
+++ b/.github/workflows/tools_tests.yaml
@@ -20,23 +20,6 @@ env:
jobs:
- plotjuggler:
- name: plotjuggler
- runs-on: ubuntu-latest
- timeout-minutes: 45
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: true
- - uses: ./.github/workflows/setup-with-retry
- - name: Build openpilot
- timeout-minutes: 5
- run: ${{ env.RUN }} "scons -j$(nproc) cereal/ common/ opendbc/ --minimal"
- - name: Test PlotJuggler
- timeout-minutes: 2
- run: |
- ${{ env.RUN }} "pytest tools/plotjuggler/"
-
simulator_build:
name: simulator docker build
runs-on: ubuntu-latest
@@ -66,8 +49,6 @@ jobs:
submodules: true
- run: git lfs pull
- uses: ./.github/workflows/setup-with-retry
- - name: Build base docker image
- run: eval "$BUILD"
- name: Build openpilot
run: |
${{ env.RUN }} "scons -j$(nproc)"
@@ -76,7 +57,7 @@ jobs:
${{ env.RUN }} "export MAPBOX_TOKEN='pk.eyJ1Ijoiam5ld2IiLCJhIjoiY2xxNW8zZXprMGw1ZzJwbzZneHd2NHljbSJ9.gV7VPRfbXFetD-1OVF0XZg' && \
source selfdrive/test/setup_xvfb.sh && \
source selfdrive/test/setup_vsound.sh && \
- CI=1 tools/sim/tests/test_metadrive_bridge.py"
+ CI=1 pytest tools/sim/tests/test_metadrive_bridge.py"
devcontainer:
name: devcontainer
@@ -92,7 +73,7 @@ jobs:
- name: Setup Dev Container CLI
run: npm install -g @devcontainers/cli
- name: Build dev container image
- run: devcontainer build --workspace-folder .
+ run: ./scripts/retry.sh devcontainer build --workspace-folder .
- name: Run dev container
run: |
mkdir -p /tmp/devcontainer_scons_cache/
@@ -104,21 +85,3 @@ jobs:
devcontainer exec --workspace-folder . pip install pip-install-test
devcontainer exec --workspace-folder . touch /home/batman/.comma/auth.json
devcontainer exec --workspace-folder . sudo touch /root/test.txt
-
- notebooks:
- name: notebooks
- runs-on: ubuntu-latest
- if: false && github.repository == 'commaai/openpilot'
- timeout-minutes: 45
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: true
- - uses: ./.github/workflows/setup-with-retry
- - name: Build openpilot
- timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache
- run: ${{ env.RUN }} "scons -j$(nproc)"
- - name: Test notebooks
- timeout-minutes: 3
- run: |
- ${{ env.RUN }} "pip install nbmake && pytest --nbmake tools/car_porting/examples/"
diff --git a/.gitignore b/.gitignore
index 98fc70d029..693c532c54 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,12 +55,8 @@ selfdrive/modeld/_modeld
selfdrive/modeld/_dmonitoringmodeld
/src/
-one
notebooks
-xx
-yy
hyperthneed
-panda_jungle
provisioning
.coverage*
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e9c7d93d88..28f8acd6e2 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -33,7 +33,7 @@ repos:
- -L bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints
- --builtins clear,rare,informal,usage,code,names,en-GB_to_en-US
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.4.3
+ rev: v0.4.4
hooks:
- id: ruff
exclude: '^(third_party/)|(cereal/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)'
@@ -47,7 +47,7 @@ repos:
args:
- --local-partial-types
- --explicit-package-bases
- exclude: '^(third_party/)|(cereal/)|(opendbc/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)|(xx/)'
+ exclude: '^(third_party/)|(body/)|(cereal/)|(opendbc/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)'
- repo: local
hooks:
- id: cppcheck
@@ -89,7 +89,7 @@ repos:
entry: selfdrive/ui/tests/test_translations.py
language: script
pass_filenames: false
- files: 'selfdrive/ui/translations/*'
+ files: '^selfdrive/ui/translations/'
- repo: https://github.com/python-poetry/poetry
rev: '1.8.0'
hooks:
@@ -98,6 +98,6 @@ repos:
args:
- --lock
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.28.2
+ rev: 0.28.3
hooks:
- id: check-github-workflows
diff --git a/Dockerfile.openpilot_base b/Dockerfile.openpilot_base
index a6c4c71790..0789a39c3e 100644
--- a/Dockerfile.openpilot_base
+++ b/Dockerfile.openpilot_base
@@ -13,14 +13,10 @@ ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
COPY tools/install_ubuntu_dependencies.sh /tmp/tools/
-RUN cd /tmp && \
- tools/install_ubuntu_dependencies.sh && \
- rm -rf /var/lib/apt/lists/* && \
- rm -rf /tmp/* && \
- # remove unused architectures from gcc for panda
+RUN INSTALL_EXTRA_PACKAGES=no /tmp/tools/install_ubuntu_dependencies.sh && \
+ rm -rf /var/lib/apt/lists/* /tmp/* && \
cd /usr/lib/gcc/arm-none-eabi/9.2.1 && \
- rm -rf arm/ && \
- rm -rf thumb/nofp thumb/v6* thumb/v8* thumb/v7+fp thumb/v7-r+fp.sp
+ rm -rf arm/ thumb/nofp thumb/v6* thumb/v8* thumb/v7+fp thumb/v7-r+fp.sp
# Add OpenCL
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -83,4 +79,4 @@ RUN cd /tmp && \
rm -rf /home/$USER/pyenv/versions/3.11.4/lib/python3.11/test
USER root
-RUN sudo git config --global --add safe.directory /tmp/openpilot
\ No newline at end of file
+RUN sudo git config --global --add safe.directory /tmp/openpilot
diff --git a/Jenkinsfile b/Jenkinsfile
index 9d12b77746..0a044ff039 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -183,7 +183,7 @@ node {
deviceStage("onroad", "tici-needs-can", [], [
// TODO: ideally, this test runs in master-ci, but it takes 5+m to build it
//["build master-ci", "cd $SOURCE_DIR/release && TARGET_DIR=$TEST_DIR $SOURCE_DIR/scripts/retry.sh ./build_devel.sh"],
- ["build openpilot", "cd selfdrive/manager && ./build.py"],
+ ["build openpilot", "cd system/manager && ./build.py"],
["check dirty", "release/check-dirty.sh"],
["onroad tests", "pytest selfdrive/test/test_onroad.py -s"],
["time to onroad", "pytest selfdrive/test/test_time_to_onroad.py"],
@@ -191,52 +191,53 @@ node {
},
'HW + Unit Tests': {
deviceStage("tici-hardware", "tici-common", ["UNSAFE=1"], [
- ["build", "cd selfdrive/manager && ./build.py"],
+ ["build", "cd system/manager && ./build.py"],
["test pandad", "pytest selfdrive/boardd/tests/test_pandad.py"],
["test power draw", "pytest -s system/hardware/tici/tests/test_power_draw.py"],
["test encoder", "LD_LIBRARY_PATH=/usr/local/lib pytest system/loggerd/tests/test_encoder.py"],
["test pigeond", "pytest system/ubloxd/tests/test_pigeond.py"],
- ["test manager", "pytest selfdrive/manager/test/test_manager.py"],
+ ["test manager", "pytest system/manager/test/test_manager.py"],
])
},
'loopback': {
deviceStage("loopback", "tici-loopback", ["UNSAFE=1"], [
- ["build openpilot", "cd selfdrive/manager && ./build.py"],
+ ["build openpilot", "cd system/manager && ./build.py"],
["test boardd loopback", "pytest selfdrive/boardd/tests/test_boardd_loopback.py"],
])
},
'camerad': {
deviceStage("AR0231", "tici-ar0231", ["UNSAFE=1"], [
- ["build", "cd selfdrive/manager && ./build.py"],
+ ["build", "cd system/manager && ./build.py"],
["test camerad", "pytest system/camerad/test/test_camerad.py"],
["test exposure", "pytest system/camerad/test/test_exposure.py"],
])
deviceStage("OX03C10", "tici-ox03c10", ["UNSAFE=1"], [
- ["build", "cd selfdrive/manager && ./build.py"],
+ ["build", "cd system/manager && ./build.py"],
["test camerad", "pytest system/camerad/test/test_camerad.py"],
["test exposure", "pytest system/camerad/test/test_exposure.py"],
])
},
'sensord': {
deviceStage("LSM + MMC", "tici-lsmc", ["UNSAFE=1"], [
- ["build", "cd selfdrive/manager && ./build.py"],
+ ["build", "cd system/manager && ./build.py"],
["test sensord", "pytest system/sensord/tests/test_sensord.py"],
])
deviceStage("BMX + LSM", "tici-bmx-lsm", ["UNSAFE=1"], [
- ["build", "cd selfdrive/manager && ./build.py"],
+ ["build", "cd system/manager && ./build.py"],
["test sensord", "pytest system/sensord/tests/test_sensord.py"],
])
},
'replay': {
deviceStage("model-replay", "tici-replay", ["UNSAFE=1"], [
- ["build", "cd selfdrive/manager && ./build.py"],
+ ["build", "cd system/manager && ./build.py"],
["model replay", "selfdrive/test/process_replay/model_replay.py"],
])
},
'tizi': {
deviceStage("tizi", "tizi", ["UNSAFE=1"], [
- ["build openpilot", "cd selfdrive/manager && ./build.py"],
+ ["build openpilot", "cd system/manager && ./build.py"],
["test boardd loopback", "SINGLE_PANDA=1 pytest selfdrive/boardd/tests/test_boardd_loopback.py"],
+ ["test boardd spi", "pytest selfdrive/boardd/tests/test_boardd_spi.py"],
["test pandad", "pytest selfdrive/boardd/tests/test_pandad.py"],
["test amp", "pytest system/hardware/tici/tests/test_amplifier.py"],
["test hw", "pytest system/hardware/tici/tests/test_hardware.py"],
diff --git a/RELEASES.md b/RELEASES.md
index da22a87a47..971ba7867d 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -1,9 +1,9 @@
-Version 0.9.7 (2024-05-XX)
+Version 0.9.7 (2024-06-11)
========================
* New driving model
* Adjust driving personality with the follow distance button
-* Support for hybrid variants of supported Ford models
* Added toggle to enable driver monitoring even when openpilot is not engaged
+* Support for hybrid variants of supported Ford models
* Fingerprinting without the OBD-II port on all cars
Version 0.9.6 (2024-02-27)
diff --git a/cereal b/cereal
index 5ce28549ba..53cdd80fa8 160000
--- a/cereal
+++ b/cereal
@@ -1 +1 @@
-Subproject commit 5ce28549ba09cdf199f5a0edaf7dc8e9b8a53344
+Subproject commit 53cdd80fa87725334df67ca9667ba0582d9b2e00
diff --git a/common/api/__init__.py b/common/api/__init__.py
index 79875023a2..f3a7d83842 100644
--- a/common/api/__init__.py
+++ b/common/api/__init__.py
@@ -7,7 +7,7 @@ from openpilot.system.version import get_version
API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com')
-class Api():
+class Api:
def __init__(self, dongle_id):
self.dongle_id = dongle_id
with open(Paths.persist_root()+'/comma/id_rsa') as f:
diff --git a/common/gpio.py b/common/gpio.py
index 68932cb87a..66b2adf438 100644
--- a/common/gpio.py
+++ b/common/gpio.py
@@ -1,5 +1,5 @@
import os
-from functools import lru_cache
+from functools import cache
def gpio_init(pin: int, output: bool) -> None:
try:
@@ -35,7 +35,7 @@ def gpio_export(pin: int) -> None:
except Exception:
print(f"Failed to export gpio {pin}")
-@lru_cache(maxsize=None)
+@cache
def get_irq_action(irq: int) -> list[str]:
try:
with open(f"/sys/kernel/irq/{irq}/actions") as f:
diff --git a/common/prefix.h b/common/prefix.h
index f5abe14b2b..11d0bc1066 100644
--- a/common/prefix.h
+++ b/common/prefix.h
@@ -5,6 +5,7 @@
#include "common/params.h"
#include "common/util.h"
+#include "system/hardware/hw.h"
class OpenpilotPrefix {
public:
@@ -25,6 +26,10 @@ public:
system(util::string_format("rm %s -rf", real_path.c_str()).c_str());
unlink(param_path.c_str());
}
+ if (getenv("COMMA_CACHE") == nullptr) {
+ system(util::string_format("rm %s -rf", Path::download_cache_root().c_str()).c_str());
+ }
+ system(util::string_format("rm %s -rf", Path::comma_home().c_str()).c_str());
system(util::string_format("rm %s -rf", msgq_path.c_str()).c_str());
unsetenv("OPENPILOT_PREFIX");
}
diff --git a/common/spinner.py b/common/spinner.py
index 43d4bb2cc2..dcf22641c4 100644
--- a/common/spinner.py
+++ b/common/spinner.py
@@ -3,7 +3,7 @@ import subprocess
from openpilot.common.basedir import BASEDIR
-class Spinner():
+class Spinner:
def __init__(self):
try:
self.spinner_proc = subprocess.Popen(["./spinner"],
diff --git a/common/stat_live.py b/common/stat_live.py
index a91c1819bb..3901c448d8 100644
--- a/common/stat_live.py
+++ b/common/stat_live.py
@@ -1,6 +1,6 @@
import numpy as np
-class RunningStat():
+class RunningStat:
# tracks realtime mean and standard deviation without storing any data
def __init__(self, priors=None, max_trackable=-1):
self.max_trackable = max_trackable
@@ -51,7 +51,7 @@ class RunningStat():
def params_to_save(self):
return [self.M, self.S, self.n]
-class RunningStatFilter():
+class RunningStatFilter:
def __init__(self, raw_priors=None, filtered_priors=None, max_trackable=-1):
self.raw_stat = RunningStat(raw_priors, -1)
self.filtered_stat = RunningStat(filtered_priors, max_trackable)
diff --git a/common/tests/test_file_helpers.py b/common/tests/test_file_helpers.py
index 1817f77cd2..a9977c2362 100644
--- a/common/tests/test_file_helpers.py
+++ b/common/tests/test_file_helpers.py
@@ -1,11 +1,10 @@
import os
-import unittest
from uuid import uuid4
from openpilot.common.file_helpers import atomic_write_in_dir
-class TestFileHelpers(unittest.TestCase):
+class TestFileHelpers:
def run_atomic_write_func(self, atomic_write_func):
path = f"/tmp/tmp{uuid4()}"
with atomic_write_func(path) as f:
@@ -13,12 +12,8 @@ class TestFileHelpers(unittest.TestCase):
assert not os.path.exists(path)
with open(path) as f:
- self.assertEqual(f.read(), "test")
+ assert f.read() == "test"
os.remove(path)
def test_atomic_write_in_dir(self):
self.run_atomic_write_func(atomic_write_in_dir)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/common/tests/test_numpy_fast.py b/common/tests/test_numpy_fast.py
index de7bb972e7..aa53851db0 100644
--- a/common/tests/test_numpy_fast.py
+++ b/common/tests/test_numpy_fast.py
@@ -1,10 +1,9 @@
import numpy as np
-import unittest
from openpilot.common.numpy_fast import interp
-class InterpTest(unittest.TestCase):
+class TestInterp:
def test_correctness_controls(self):
_A_CRUISE_MIN_BP = np.asarray([0., 5., 10., 20., 40.])
_A_CRUISE_MIN_V = np.asarray([-1.0, -.8, -.67, -.5, -.30])
@@ -20,7 +19,3 @@ class InterpTest(unittest.TestCase):
expected = np.interp(v_ego, _A_CRUISE_MIN_BP, _A_CRUISE_MIN_V)
actual = interp(v_ego, _A_CRUISE_MIN_BP, _A_CRUISE_MIN_V)
np.testing.assert_equal(actual, expected)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/common/tests/test_params.py b/common/tests/test_params.py
index 490ee122be..16cbc45295 100644
--- a/common/tests/test_params.py
+++ b/common/tests/test_params.py
@@ -1,13 +1,13 @@
+import pytest
import os
import threading
import time
import uuid
-import unittest
from openpilot.common.params import Params, ParamKeyType, UnknownKeyName
-class TestParams(unittest.TestCase):
- def setUp(self):
+class TestParams:
+ def setup_method(self):
self.params = Params()
def test_params_put_and_get(self):
@@ -49,16 +49,16 @@ class TestParams(unittest.TestCase):
assert self.params.get("CarParams", True) == b"test"
def test_params_unknown_key_fails(self):
- with self.assertRaises(UnknownKeyName):
+ with pytest.raises(UnknownKeyName):
self.params.get("swag")
- with self.assertRaises(UnknownKeyName):
+ with pytest.raises(UnknownKeyName):
self.params.get_bool("swag")
- with self.assertRaises(UnknownKeyName):
+ with pytest.raises(UnknownKeyName):
self.params.put("swag", "abc")
- with self.assertRaises(UnknownKeyName):
+ with pytest.raises(UnknownKeyName):
self.params.put_bool("swag", True)
def test_remove_not_there(self):
@@ -68,19 +68,19 @@ class TestParams(unittest.TestCase):
def test_get_bool(self):
self.params.remove("IsMetric")
- self.assertFalse(self.params.get_bool("IsMetric"))
+ assert not self.params.get_bool("IsMetric")
self.params.put_bool("IsMetric", True)
- self.assertTrue(self.params.get_bool("IsMetric"))
+ assert self.params.get_bool("IsMetric")
self.params.put_bool("IsMetric", False)
- self.assertFalse(self.params.get_bool("IsMetric"))
+ assert not self.params.get_bool("IsMetric")
self.params.put("IsMetric", "1")
- self.assertTrue(self.params.get_bool("IsMetric"))
+ assert self.params.get_bool("IsMetric")
self.params.put("IsMetric", "0")
- self.assertFalse(self.params.get_bool("IsMetric"))
+ assert not self.params.get_bool("IsMetric")
def test_put_non_blocking_with_get_block(self):
q = Params()
@@ -107,7 +107,3 @@ class TestParams(unittest.TestCase):
assert len(keys) > 20
assert len(keys) == len(set(keys))
assert b"CarParams" in keys
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/common/tests/test_simple_kalman.py b/common/tests/test_simple_kalman.py
index f641cd19e6..f4a967e58a 100644
--- a/common/tests/test_simple_kalman.py
+++ b/common/tests/test_simple_kalman.py
@@ -1,10 +1,8 @@
-import unittest
-
from openpilot.common.simple_kalman import KF1D
-class TestSimpleKalman(unittest.TestCase):
- def setUp(self):
+class TestSimpleKalman:
+ def setup_method(self):
dt = 0.01
x0_0 = 0.0
x1_0 = 0.0
@@ -24,12 +22,8 @@ class TestSimpleKalman(unittest.TestCase):
def test_getter_setter(self):
self.kf.set_x([[1.0], [1.0]])
- self.assertEqual(self.kf.x, [[1.0], [1.0]])
+ assert self.kf.x == [[1.0], [1.0]]
def update_returns_state(self):
x = self.kf.update(100)
- self.assertEqual(x, self.kf.x)
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert x == self.kf.x
diff --git a/common/transformations/tests/test_coordinates.py b/common/transformations/tests/test_coordinates.py
index 7ae79403bd..41076d9b3f 100755
--- a/common/transformations/tests/test_coordinates.py
+++ b/common/transformations/tests/test_coordinates.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import numpy as np
-import unittest
import openpilot.common.transformations.coordinates as coord
@@ -44,7 +43,7 @@ ned_offsets_batch = np.array([[ 53.88103168, 43.83445935, -46.27488057],
[ 78.56272609, 18.53100158, -43.25290759]])
-class TestNED(unittest.TestCase):
+class TestNED:
def test_small_distances(self):
start_geodetic = np.array([33.8042184, -117.888593, 0.0])
local_coord = coord.LocalCoord.from_geodetic(start_geodetic)
@@ -54,13 +53,13 @@ class TestNED(unittest.TestCase):
west_geodetic = start_geodetic + [0, -0.0005, 0]
west_ned = local_coord.geodetic2ned(west_geodetic)
- self.assertLess(np.abs(west_ned[0]), 1e-3)
- self.assertLess(west_ned[1], 0)
+ assert np.abs(west_ned[0]) < 1e-3
+ assert west_ned[1] < 0
southwest_geodetic = start_geodetic + [-0.0005, -0.002, 0]
southwest_ned = local_coord.geodetic2ned(southwest_geodetic)
- self.assertLess(southwest_ned[0], 0)
- self.assertLess(southwest_ned[1], 0)
+ assert southwest_ned[0] < 0
+ assert southwest_ned[1] < 0
def test_ecef_geodetic(self):
# testing single
@@ -105,5 +104,3 @@ class TestNED(unittest.TestCase):
np.testing.assert_allclose(converter.ned2ecef(ned_offsets_batch),
ecef_positions_offset_batch,
rtol=1e-9, atol=1e-7)
-if __name__ == "__main__":
- unittest.main()
diff --git a/common/transformations/tests/test_orientation.py b/common/transformations/tests/test_orientation.py
index f77827d2f9..695642774e 100755
--- a/common/transformations/tests/test_orientation.py
+++ b/common/transformations/tests/test_orientation.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import numpy as np
-import unittest
from openpilot.common.transformations.orientation import euler2quat, quat2euler, euler2rot, rot2euler, \
rot2quat, quat2rot, \
@@ -32,7 +31,7 @@ ned_eulers = np.array([[ 0.46806039, -0.4881889 , 1.65697808],
[ 2.50450101, 0.36304151, 0.33136365]])
-class TestOrientation(unittest.TestCase):
+class TestOrientation:
def test_quat_euler(self):
for i, eul in enumerate(eulers):
np.testing.assert_allclose(quats[i], euler2quat(eul), rtol=1e-7)
@@ -62,7 +61,3 @@ class TestOrientation(unittest.TestCase):
np.testing.assert_allclose(ned_eulers[i], ned_euler_from_ecef(ecef_positions[i], eulers[i]), rtol=1e-7)
#np.testing.assert_allclose(eulers[i], ecef_euler_from_ned(ecef_positions[i], ned_eulers[i]), rtol=1e-7)
# np.testing.assert_allclose(ned_eulers, ned_euler_from_ecef(ecef_positions, eulers), rtol=1e-7)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/conftest.py b/conftest.py
index 0d2a4a8fc4..fc4931fb54 100644
--- a/conftest.py
+++ b/conftest.py
@@ -5,7 +5,7 @@ import pytest
import random
from openpilot.common.prefix import OpenpilotPrefix
-from openpilot.selfdrive.manager import manager
+from openpilot.system.manager import manager
from openpilot.system.hardware import TICI, HARDWARE
diff --git a/docs/BOUNTIES.md b/docs/BOUNTIES.md
index 7e7ee3b8d3..9d0718c572 100644
--- a/docs/BOUNTIES.md
+++ b/docs/BOUNTIES.md
@@ -9,6 +9,7 @@ Get paid to improve openpilot!
* once you open a PR, the bounty is locked to you until you stop working on it
* open a ticket at [comma.ai/support](https://comma.ai/support/shop-order) with links to your PRs to claim
* get an extra 20% if you redeem your bounty in [comma shop](https://comma.ai/shop) credit (including refunds on previous orders)
+* for bounties >$100, the first PR gets a lock, which times out after a week of no progress
We put up each bounty with the intention that it'll get merged, but occasionally the right resolution is to close the bounty, which only becomes clear once some effort is put in.
This is still valuable work, so we'll pay out $100 for getting any bounty closed with a good explanation.
diff --git a/docs/CARS.md b/docs/CARS.md
index 7e5324d322..26b7fe203f 100644
--- a/docs/CARS.md
+++ b/docs/CARS.md
@@ -4,7 +4,7 @@
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
-# 286 Supported Cars
+# 288 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|
Hardware Needed
|Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@@ -49,7 +49,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Genesis|G70 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Genesis|G70 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Genesis|G80 2017|All|Stock|19 mph|37 mph|[](##)|[](##)|Parts
- 1 Hyundai J connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Genesis|G80 2017|All|Stock|19 mph|37 mph|[](##)|[](##)|Parts
- 1 Hyundai J connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Genesis|G80 2018-19|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Genesis|G90 2017-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Genesis|GV60 (Advanced Trim) 2023[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
@@ -83,12 +83,12 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Azera Hybrid 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Azera Hybrid 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Custin 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Hyundai|Elantra 2017-18|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai B connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Hyundai|Elantra 2019|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai G connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Hyundai|Elantra 2017-18|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai B connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Hyundai|Elantra 2019|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai G connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Elantra 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Hyundai|Elantra GT 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
-|Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[](##)|[](##)|Parts
- 1 Hyundai J connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[](##)|[](##)|Parts
- 1 Hyundai J connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Ioniq 5 (Southeast Asia only) 2022-23[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Ioniq 5 (with HDA II) 2022-23[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
@@ -100,7 +100,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Ioniq Hybrid 2020-22|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai B connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai B connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai G connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai O connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai R connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
@@ -115,22 +115,23 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Sonata 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Hyundai|Sonata Hybrid 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Staria 2023[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Hyundai|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Hyundai|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Tucson 2022[5](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Tucson 2023-24[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Tucson Diesel 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Hyundai|Tucson Hybrid 2022-24[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Hyundai|Tucson Plug-in Hybrid 2024[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai N connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[](##)|[](##)|Parts
- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|Parts
- 1 FCA connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Kia|Carnival 2022-24[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Kia|Carnival (China only) 2023[5](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Kia|Ceed 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Kia|EV6 (Southeast Asia only) 2022-23[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai P connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Kia|EV6 (with HDA II) 2022-23[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai P connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Kia|EV6 (without HDA II) 2022-23[5](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai G connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Kia|Forte 2023|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Kia|EV6 (Southeast Asia only) 2022-24[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai P connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Kia|EV6 (with HDA II) 2022-24[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai P connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Kia|EV6 (without HDA II) 2022-24[5](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai G connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Kia|K8 Hybrid (with HDA II) 2023[5](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai Q connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
@@ -139,11 +140,11 @@ A supported vehicle is one that just works when you install a comma device. All
|Kia|Niro EV 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Kia|Niro EV 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Kia|Niro EV 2023[5](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Kia|Niro Hybrid 2018|All|Stock|10 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Kia|Niro Hybrid 2018|All|Stock|10 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Kia|Niro Hybrid 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai D connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Kia|Niro Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Kia|Niro Hybrid 2023[5](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Kia|Niro Plug-in Hybrid 2018-19|All|Stock|10 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Kia|Niro Plug-in Hybrid 2018-19|All|Stock|10 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Kia|Niro Plug-in Hybrid 2020|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai D connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Kia|Niro Plug-in Hybrid 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai D connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai F connector
- 1 RJ45 cable (7 ft)
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
@@ -232,14 +233,14 @@ A supported vehicle is one that just works when you install a comma device. All
|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Toyota|Corolla 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
-|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Toyota|Corolla Hybrid (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Toyota|Highlander 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
+|Toyota|Corolla Hybrid (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Toyota|Highlander 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Toyota|Highlander Hybrid 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Toyota|Highlander Hybrid 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Toyota|Prius 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
@@ -247,21 +248,22 @@ A supported vehicle is one that just works when you install a comma device. All
|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Toyota|Prius Prime 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
-|Toyota|Prius v 2017|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
+|Toyota|Prius v 2017|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Toyota|RAV4 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Toyota|RAV4 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Toyota|RAV4 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Toyota|RAV4 2023-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
-|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
+|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
+|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Toyota|RAV4 Hybrid 2023-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
-|Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
+|Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 RJ45 cable (7 ft)
- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v2
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
+|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here |
|
|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 J533 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,12](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 J533 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable
- 1 right angle OBD-C cable (1.5 ft)
Buy Here ||
diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh
index a7f61411c8..9256f463af 100755
--- a/launch_chffrplus.sh
+++ b/launch_chffrplus.sh
@@ -82,7 +82,7 @@ function launch {
tmux capture-pane -pq -S-1000 > /tmp/launch_log
# start manager
- cd selfdrive/manager
+ cd system/manager
if [ ! -f $DIR/prebuilt ]; then
./build.py
fi
diff --git a/opendbc b/opendbc
index 9876e5827d..0c00eae7a5 160000
--- a/opendbc
+++ b/opendbc
@@ -1 +1 @@
-Subproject commit 9876e5827d32f5c40a236e1cc7ee0598b9bea285
+Subproject commit 0c00eae7a526882ae82185b8a497e673dd8157f1
diff --git a/panda b/panda
index a6eb8dd7be..3d77f11ca1 160000
--- a/panda
+++ b/panda
@@ -1 +1 @@
-Subproject commit a6eb8dd7be07d066fcd72ec9ad82970fdf2db76e
+Subproject commit 3d77f11ca169a7af435fa078039942dfbf809838
diff --git a/poetry.lock b/poetry.lock
index 70e91f2cc2..23b2eb7b8a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -291,23 +291,23 @@ msal-extensions = ">=0.3.0"
[[package]]
name = "azure-storage-blob"
-version = "12.19.1"
+version = "12.20.0"
description = "Microsoft Azure Blob Storage Client Library for Python"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "azure-storage-blob-12.19.1.tar.gz", hash = "sha256:13e16ba42fc54ac2c7e8f976062173a5c82b9ec0594728e134aac372965a11b0"},
- {file = "azure_storage_blob-12.19.1-py3-none-any.whl", hash = "sha256:c5530dc51c21c9564e4eb706cd499befca8819b10dd89716d3fc90d747556243"},
+ {file = "azure-storage-blob-12.20.0.tar.gz", hash = "sha256:eeb91256e41d4b5b9bad6a87fd0a8ade07dd58aa52344e2c8d2746e27a017d3b"},
+ {file = "azure_storage_blob-12.20.0-py3-none-any.whl", hash = "sha256:de6b3bf3a90e9341a6bcb96a2ebe981dffff993e9045818f6549afea827a52a9"},
]
[package.dependencies]
-azure-core = ">=1.28.0,<2.0.0"
+azure-core = ">=1.28.0"
cryptography = ">=2.1.4"
isodate = ">=0.6.1"
-typing-extensions = ">=4.3.0"
+typing-extensions = ">=4.6.0"
[package.extras]
-aio = ["azure-core[aio] (>=1.28.0,<2.0.0)"]
+aio = ["azure-core[aio] (>=1.28.0)"]
[[package]]
name = "babel"
@@ -830,43 +830,43 @@ files = [
[[package]]
name = "cryptography"
-version = "42.0.6"
+version = "42.0.7"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
- {file = "cryptography-42.0.6-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:073104df012fc815eed976cd7d0a386c8725d0d0947cf9c37f6c36a6c20feb1b"},
- {file = "cryptography-42.0.6-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5967e3632f42b0c0f9dc2c9da88c79eabdda317860b246d1fbbde4a8bbbc3b44"},
- {file = "cryptography-42.0.6-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99831397fdc6e6e0aa088b060c278c6e635d25c0d4d14bdf045bf81792fda0a"},
- {file = "cryptography-42.0.6-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:089aeb297ff89615934b22c7631448598495ffd775b7d540a55cfee35a677bf4"},
- {file = "cryptography-42.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:97eeacae9aa526ddafe68b9202a535f581e21d78f16688a84c8dcc063618e121"},
- {file = "cryptography-42.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f4cece02478d73dacd52be57a521d168af64ae03d2a567c0c4eb6f189c3b9d79"},
- {file = "cryptography-42.0.6-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb6f56b004e898df5530fa873e598ec78eb338ba35f6fa1449970800b1d97c2"},
- {file = "cryptography-42.0.6-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8b90c57b3cd6128e0863b894ce77bd36fcb5f430bf2377bc3678c2f56e232316"},
- {file = "cryptography-42.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d16a310c770cc49908c500c2ceb011f2840674101a587d39fa3ea828915b7e83"},
- {file = "cryptography-42.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3442601d276bd9e961d618b799761b4e5d892f938e8a4fe1efbe2752be90455"},
- {file = "cryptography-42.0.6-cp37-abi3-win32.whl", hash = "sha256:00c0faa5b021457848d031ecff041262211cc1e2bce5f6e6e6c8108018f6b44a"},
- {file = "cryptography-42.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:b16b90605c62bcb3aa7755d62cf5e746828cfc3f965a65211849e00c46f8348d"},
- {file = "cryptography-42.0.6-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:eecca86813c6a923cabff284b82ff4d73d9e91241dc176250192c3a9b9902a54"},
- {file = "cryptography-42.0.6-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d93080d2b01b292e7ee4d247bf93ed802b0100f5baa3fa5fd6d374716fa480d4"},
- {file = "cryptography-42.0.6-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ff75b88a4d273c06d968ad535e6cb6a039dd32db54fe36f05ed62ac3ef64a44"},
- {file = "cryptography-42.0.6-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c05230d8aaaa6b8ab3ab41394dc06eb3d916131df1c9dcb4c94e8f041f704b74"},
- {file = "cryptography-42.0.6-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9184aff0856261ecb566a3eb26a05dfe13a292c85ce5c59b04e4aa09e5814187"},
- {file = "cryptography-42.0.6-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:4bdb39ecbf05626e4bfa1efd773bb10346af297af14fb3f4c7cb91a1d2f34a46"},
- {file = "cryptography-42.0.6-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e85f433230add2aa26b66d018e21134000067d210c9c68ef7544ba65fc52e3eb"},
- {file = "cryptography-42.0.6-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:65d529c31bd65d54ce6b926a01e1b66eacf770b7e87c0622516a840e400ec732"},
- {file = "cryptography-42.0.6-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f1e933b238978ccfa77b1fee0a297b3c04983f4cb84ae1c33b0ea4ae08266cc9"},
- {file = "cryptography-42.0.6-cp39-abi3-win32.whl", hash = "sha256:bc954251edcd8a952eeaec8ae989fec7fe48109ab343138d537b7ea5bb41071a"},
- {file = "cryptography-42.0.6-cp39-abi3-win_amd64.whl", hash = "sha256:9f1a3bc2747166b0643b00e0b56cd9b661afc9d5ff963acaac7a9c7b2b1ef638"},
- {file = "cryptography-42.0.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:945a43ebf036dd4b43ebfbbd6b0f2db29ad3d39df824fb77476ca5777a9dde33"},
- {file = "cryptography-42.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f567a82b7c2b99257cca2a1c902c1b129787278ff67148f188784245c7ed5495"},
- {file = "cryptography-42.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3b750279f3e7715df6f68050707a0cee7cbe81ba2eeb2f21d081bd205885ffed"},
- {file = "cryptography-42.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6981acac509cc9415344cb5bfea8130096ea6ebcc917e75503143a1e9e829160"},
- {file = "cryptography-42.0.6-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:076c92b08dd1ab88108bc84545187e10d3693a9299c593f98c4ea195a0b0ead7"},
- {file = "cryptography-42.0.6-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81dbe47e28b703bc4711ac74a64ef8b758a0cf056ce81d08e39116ab4bc126fa"},
- {file = "cryptography-42.0.6-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e1f5f15c5ddadf6ee4d1d624a2ae940f14bd74536230b0056ccb28bb6248e42a"},
- {file = "cryptography-42.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:43e521f21c2458038d72e8cdfd4d4d9f1d00906a7b6636c4272e35f650d1699b"},
- {file = "cryptography-42.0.6.tar.gz", hash = "sha256:f987a244dfb0333fbd74a691c36000a2569eaf7c7cc2ac838f85f59f0588ddc9"},
+ {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"},
+ {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"},
+ {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"},
+ {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"},
+ {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"},
+ {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"},
+ {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"},
+ {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"},
+ {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"},
+ {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"},
+ {file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"},
+ {file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"},
+ {file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"},
+ {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"},
+ {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"},
+ {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"},
+ {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"},
+ {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"},
+ {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"},
+ {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"},
+ {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"},
+ {file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"},
+ {file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"},
+ {file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"},
+ {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"},
+ {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"},
+ {file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"},
+ {file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"},
+ {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"},
+ {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"},
+ {file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"},
+ {file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"},
]
[package.dependencies]
@@ -1152,53 +1152,53 @@ files = [
[[package]]
name = "fonttools"
-version = "4.51.0"
+version = "4.52.1"
description = "Tools to manipulate font files"
optional = false
python-versions = ">=3.8"
files = [
- {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74"},
- {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308"},
- {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037"},
- {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716"},
- {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438"},
- {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039"},
- {file = "fonttools-4.51.0-cp310-cp310-win32.whl", hash = "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77"},
- {file = "fonttools-4.51.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b"},
- {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74"},
- {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2"},
- {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f"},
- {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097"},
- {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0"},
- {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1"},
- {file = "fonttools-4.51.0-cp311-cp311-win32.whl", hash = "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034"},
- {file = "fonttools-4.51.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1"},
- {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba"},
- {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc"},
- {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a"},
- {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2"},
- {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671"},
- {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5"},
- {file = "fonttools-4.51.0-cp312-cp312-win32.whl", hash = "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15"},
- {file = "fonttools-4.51.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e"},
- {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e"},
- {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5"},
- {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e"},
- {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1"},
- {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14"},
- {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed"},
- {file = "fonttools-4.51.0-cp38-cp38-win32.whl", hash = "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f"},
- {file = "fonttools-4.51.0-cp38-cp38-win_amd64.whl", hash = "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836"},
- {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b"},
- {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936"},
- {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55"},
- {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce"},
- {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051"},
- {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7"},
- {file = "fonttools-4.51.0-cp39-cp39-win32.whl", hash = "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636"},
- {file = "fonttools-4.51.0-cp39-cp39-win_amd64.whl", hash = "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a"},
- {file = "fonttools-4.51.0-py3-none-any.whl", hash = "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f"},
- {file = "fonttools-4.51.0.tar.gz", hash = "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68"},
+ {file = "fonttools-4.52.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:67a30b872e79577e5319ce660ede4a5131fa8a45de76e696746545e17db4437f"},
+ {file = "fonttools-4.52.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a5bff35738f8f6607c4303561ee1d1e5f64d5b14cf3c472d3030566c82e763"},
+ {file = "fonttools-4.52.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c9622593dfff042480a1b7e5b72c4d7dc00b96d2b4f98b0bf8acf071087e0db"},
+ {file = "fonttools-4.52.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33cfc9fe27af5e113d157d5147e24fc8e5bda3c5aadb55bea9847ec55341ce30"},
+ {file = "fonttools-4.52.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa5bec5027d947ee4b2242caecf7dc6e4ea03833e92e9b5211ebb6ab4eede8b2"},
+ {file = "fonttools-4.52.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10e44bf8e5654050a332a79285bacd6bd3069084540aec46c0862391147a1daa"},
+ {file = "fonttools-4.52.1-cp310-cp310-win32.whl", hash = "sha256:7fba390ac2ca18ebdd456f3a9acfb4557d6dcb2eaba5cc3eadce01003892a770"},
+ {file = "fonttools-4.52.1-cp310-cp310-win_amd64.whl", hash = "sha256:15df3517eb95035422a5c953ca19aac99913c16aa0e4ef061aeaef5f3bcaf369"},
+ {file = "fonttools-4.52.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40730aab9cf42286f314b985b483eea574f1bcf3a23e28223084cbb9e256457c"},
+ {file = "fonttools-4.52.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a19bc2be3af5b22ff5c7fe858c380862e31052c74f62e2c6d565ed0855bed7a6"},
+ {file = "fonttools-4.52.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f859066d8afde53f2ddabcd0705061e6d9d9868757c6ae28abe49bc885292df4"},
+ {file = "fonttools-4.52.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74cd3e3e9ba501e87a391b62e91f7b1610e8b3f3d706a368e5aee51614c1674e"},
+ {file = "fonttools-4.52.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:958957b81418647f66820480363cb617ba6b5bcf189ec6c4cea307d051048545"},
+ {file = "fonttools-4.52.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56addf1f995d94dad13aaaf56eb6def3d9ca97c2fada5e27af8190b3141e8633"},
+ {file = "fonttools-4.52.1-cp311-cp311-win32.whl", hash = "sha256:fea5456b2af42db8ecb1a6c2f144655ca6dcdcebd970f3145c56e668084ded7e"},
+ {file = "fonttools-4.52.1-cp311-cp311-win_amd64.whl", hash = "sha256:228faab7638cd726cdde5e2ec9ee10f780fbf9de9aa38d7f1e56a270437dff36"},
+ {file = "fonttools-4.52.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7c6aeb0d53e2ea92009b11c3d4ad9c03d0ecdfe602d547bed8537836e464f51e"},
+ {file = "fonttools-4.52.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e871123d12c92e2c9bda6369b69ce2da9cef40b119cc340451e413e90355fa38"},
+ {file = "fonttools-4.52.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ff8857dc9bb3e407c25aef3e025409cfbb23adb646a835636bebb1bdfc27a41"},
+ {file = "fonttools-4.52.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7685fdc6e23267844eef2b9af585d7f171cca695e4eb369d7682544c3e2e1123"},
+ {file = "fonttools-4.52.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1e1b2774485fbbb41a1beccc913b9c6f7971f78da61dd34207b9acc3cc2963e"},
+ {file = "fonttools-4.52.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e2c415160397fd6ed3964155aeec4bfefceeee365ab17161a5b3fe3f8dab077"},
+ {file = "fonttools-4.52.1-cp312-cp312-win32.whl", hash = "sha256:3ba2c4647e7decfb8e9cd346661c7d151dae1fba23d37b48bcf5fa8351f7b8c8"},
+ {file = "fonttools-4.52.1-cp312-cp312-win_amd64.whl", hash = "sha256:d39b926f14a2f7a7f92ded7d266b18f0108d867364769ab59da88ac2fa90d288"},
+ {file = "fonttools-4.52.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6e58d8097a269b6c43ec0abb3fa8d6c350ff0c7dfd23fc14d004610df88a4bb3"},
+ {file = "fonttools-4.52.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:20f0fc969817c50539dc919ed8c4aef4de28c2d6e0111a064112301f157aede4"},
+ {file = "fonttools-4.52.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d62e84d38969491c6c1f6fe3dd63108e99d02de01bb3d98c160a5d4d24120910"},
+ {file = "fonttools-4.52.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb5a389bbdee6f4c422881de422ee0e7efdfcd9310b13d540b12aa8ae2c9e7b"},
+ {file = "fonttools-4.52.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0caf05c969cbde6729dd97b64bea445ee152bb19215d5886f7b93bd0fb455468"},
+ {file = "fonttools-4.52.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df08bee1dd29a767311b50c62c0cfe4d72ae8c793e567d4c60b8c16c7c63a4f0"},
+ {file = "fonttools-4.52.1-cp38-cp38-win32.whl", hash = "sha256:82ffcf4782ceda09842b5b7875b36834c15d7cc0d5dd3d23a658ee9cf8819cd6"},
+ {file = "fonttools-4.52.1-cp38-cp38-win_amd64.whl", hash = "sha256:26b43bab5a3bce55ed4d9699b16568795eef5597d154f52dcabef5b4804c4b21"},
+ {file = "fonttools-4.52.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e8dbc13c4bc12e60df1b1f5e484112a5e96a6e8bba995e2965988ad73c5ea1b"},
+ {file = "fonttools-4.52.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7352ba2226e45e8fba11c3fb416363faf1b06f3f2e80d07d2930401265f3bf9c"},
+ {file = "fonttools-4.52.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8834d43763e9e92349ce8bb25dfb612aef6691eefefad885212d5e8f36a94a4"},
+ {file = "fonttools-4.52.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2a8c1101d06cc8fca7851dceb67afd53dd6fc0288bacaa632e647bc5afff58"},
+ {file = "fonttools-4.52.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a99b738227c0f6f2bbe381b45804a7c46653c95b9d7bf13f6f02884bc87e4930"},
+ {file = "fonttools-4.52.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:75aa00a16b9a64d1550e2e70d3582c7fe1ef18560e0cf066a4087fe6d11908a2"},
+ {file = "fonttools-4.52.1-cp39-cp39-win32.whl", hash = "sha256:c2f09b4aa699cfed4bbebc1829c5f044b41976707dac9230ed00d5a9fc6452c1"},
+ {file = "fonttools-4.52.1-cp39-cp39-win_amd64.whl", hash = "sha256:78ea6e0d4c89f8e216995923b854dd10bd09e48d3a5a3ccb48bb68f436a409ad"},
+ {file = "fonttools-4.52.1-py3-none-any.whl", hash = "sha256:faf5c83f83f7ddebdafdb453d02efdbea7fb494080d7a8d45a8a20db06ea8da5"},
+ {file = "fonttools-4.52.1.tar.gz", hash = "sha256:8c9204435aa6e5e9479a5ba4e669f05dea28b0c61958e0c0923cb164296d9329"},
]
[package.extras]
@@ -1301,50 +1301,6 @@ files = [
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
]
-[[package]]
-name = "ft4222"
-version = "1.10.0"
-description = "Python wrapper around libFT4222."
-optional = false
-python-versions = "*"
-files = [
- {file = "ft4222-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd003a91ecaf20bc4d4ce8f0aea4a5ecfbc17a3840d41b0ce27410e027530cd0"},
- {file = "ft4222-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cedabcd6c554e78f3479350153c5a347355ccc977ceeb24726edc37d3e7edb7"},
- {file = "ft4222-1.10.0-cp310-cp310-win32.whl", hash = "sha256:a039d4230926fa9b600baa12903843c53d6b238eb93e99ae68eeeb450d6f4bdc"},
- {file = "ft4222-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b05e499fd3f1baef225671f3afd25bf93e1f287fa17326c90c1f8db70abb3e89"},
- {file = "ft4222-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df9da4d5575d51511ba58e73ee434061dc709ce69491656ec109d995ce304167"},
- {file = "ft4222-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20f2cb89e63ea755d69a4d41c2dcba94d2f720b6e736ea51bdf681f1e809763c"},
- {file = "ft4222-1.10.0-cp311-cp311-win32.whl", hash = "sha256:806d4117d0814c390c7f280345aa99168ec989ca49b20259ce593ea79bcd246f"},
- {file = "ft4222-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:ad626507e9b1520f2f3b6f98ddca0d257c93b844824a1ab7cbb58759b0fbca84"},
- {file = "ft4222-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ecaae4e5ec3bedcdbe04a5792bf08119040669cf58c02df947395e6a96bd18c"},
- {file = "ft4222-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8c2655fe7192eb80ef25ab750123139f591e0f079fe2a5e6f31b7cd6f015a9f"},
- {file = "ft4222-1.10.0-cp312-cp312-win32.whl", hash = "sha256:fe668caa43bcacfbf3287cff8b4b11bb586a71d4722307e574f60180bb4a351e"},
- {file = "ft4222-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:4d7a49febc2b96ebed4b843262ab1f00843d158d6f4877c8ee19219f1c41f0da"},
- {file = "ft4222-1.10.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a6f99e31c767b6ccb7e2f0e7f6cc51d15c9b685e2b81fd12ab08cac0924289"},
- {file = "ft4222-1.10.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efcb50649a7ab4c7e045467357029f16ef0be064d46ffe7ea1e06eb9ad85f8"},
- {file = "ft4222-1.10.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e051c89cb45f964640e355506efa74b41c424e9ea9046367534180075be0a95c"},
- {file = "ft4222-1.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cae991a917602635238c144395bb59a516b7684b6599da251a88cfe37a5ed8f"},
- {file = "ft4222-1.10.0-cp37-cp37m-win32.whl", hash = "sha256:713ed85fbb8f11755ae7ed932302192a34d2aa8834ffa45a7c345d3a5ba8f67b"},
- {file = "ft4222-1.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f2458e6c62d251b6219cfa0fbcfb9ccfa22854c5d7463b862f11b268019ec66"},
- {file = "ft4222-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4b489cd434b92748e45695c1313716db4e00bdda4f70b6bcf99cdc042a34d8"},
- {file = "ft4222-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0bd0632063b0e4bb2852ccde3fb95252ea7f0a0bfd3e4c3ee677f4ec63c2520"},
- {file = "ft4222-1.10.0-cp38-cp38-win32.whl", hash = "sha256:057c88eaa2db14a05163b90a7ba2938c0aa6e3870fec31c70051fb791cfbbc22"},
- {file = "ft4222-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:648a739bb5c0ca53b5cb0698e0ebb54ddaea090878a02cbe9039b3d80cf818ba"},
- {file = "ft4222-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88170a0aaa321ae8c974a664b2316fab00f86dd8249ff5927ea4d58a2ed7fa23"},
- {file = "ft4222-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69fa55d8b5bc7da5a20254d480d4764dbcaa1ca74cd2d10bbc0ce39d39891f2a"},
- {file = "ft4222-1.10.0-cp39-cp39-win32.whl", hash = "sha256:9eb40e86fd43ab8d12b33d092369419e275292a6d57f1ef7de30e63cd8dc9bd5"},
- {file = "ft4222-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:875478f2c8c0fa128037c389969375ede546dde8cf7a5ae796421832aa6dd634"},
- {file = "ft4222-1.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:834b9e549d66f3cc0bbe75983b32e756258523b70363bb67970a7be21c9e3d8f"},
- {file = "ft4222-1.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7af52e4d61e2bcaa10b802e6f1af29823da12a27f520c799823490e6fd9ebb19"},
- {file = "ft4222-1.10.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca8c5aff74e213d8b658e7cdd8df3012e55023a2d379b5074628f6bb8947359a"},
- {file = "ft4222-1.10.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:755a70ab2c5d3fd94030e51fd82c332bc2463c6c455adf093d12e2f3803ea207"},
- {file = "ft4222-1.10.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:035ea7fc9ad627b44be57c31e87bc8d445ebd7cf6ac991d750fedc999683d64e"},
- {file = "ft4222-1.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2df65d174200c1451f5646a109e683aada9d7a076bf5f7ce682145d10725445a"},
- {file = "ft4222-1.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99ef3c412017a85ef9f90d8fc884ba66dae71e85433c8890da6aec38eb0288ab"},
- {file = "ft4222-1.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d00b36bdac3960c377f3a625fc352eca20e36825ddff178feaae97c2d9a8846c"},
- {file = "ft4222-1.10.0.tar.gz", hash = "sha256:74eab32dfc5c012d11952c8645b72157f8828685972669ef3c22025491d96001"},
-]
-
[[package]]
name = "future-fstrings"
version = "1.2.0"
@@ -1954,165 +1910,153 @@ test = ["pytest"]
[[package]]
name = "lxml"
-version = "5.2.1"
+version = "5.2.2"
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
optional = false
python-versions = ">=3.6"
files = [
- {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1"},
- {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3"},
- {file = "lxml-5.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377"},
- {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3"},
- {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81"},
- {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a"},
- {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937"},
- {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8"},
- {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d"},
- {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a"},
- {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47"},
- {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d"},
- {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a"},
- {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433"},
- {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56"},
- {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052"},
- {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a"},
- {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01"},
- {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1"},
- {file = "lxml-5.2.1-cp310-cp310-win32.whl", hash = "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5"},
- {file = "lxml-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f"},
- {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"},
- {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"},
- {file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"},
- {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"},
- {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"},
- {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"},
- {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"},
- {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"},
- {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"},
- {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"},
- {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"},
- {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"},
- {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"},
- {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"},
- {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"},
- {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"},
- {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"},
- {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"},
- {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"},
- {file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"},
- {file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"},
- {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"},
- {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"},
- {file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"},
- {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"},
- {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"},
- {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"},
- {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"},
- {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"},
- {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"},
- {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"},
- {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"},
- {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"},
- {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"},
- {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"},
- {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"},
- {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"},
- {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"},
- {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"},
- {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"},
- {file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"},
- {file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"},
- {file = "lxml-5.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c"},
- {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"},
- {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"},
- {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"},
- {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"},
- {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"},
- {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"},
- {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"},
- {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45"},
- {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885"},
- {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9"},
- {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62"},
- {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461"},
- {file = "lxml-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0"},
- {file = "lxml-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289"},
- {file = "lxml-5.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029"},
- {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af"},
- {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0"},
- {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456"},
- {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd"},
- {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619"},
- {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213"},
- {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6"},
- {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04"},
- {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41"},
- {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75"},
- {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8"},
- {file = "lxml-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd"},
- {file = "lxml-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c"},
- {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b"},
- {file = "lxml-5.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a"},
- {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585"},
- {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86"},
- {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533"},
- {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739"},
- {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7"},
- {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5"},
- {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b"},
- {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b"},
- {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec"},
- {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca"},
- {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c"},
- {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637"},
- {file = "lxml-5.2.1-cp38-cp38-win32.whl", hash = "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da"},
- {file = "lxml-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806"},
- {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd"},
- {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7"},
- {file = "lxml-5.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd"},
- {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb"},
- {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc"},
- {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53"},
- {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4"},
- {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3"},
- {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913"},
- {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b"},
- {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e"},
- {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54"},
- {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57"},
- {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8"},
- {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef"},
- {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b"},
- {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919"},
- {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c"},
- {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188"},
- {file = "lxml-5.2.1-cp39-cp39-win32.whl", hash = "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708"},
- {file = "lxml-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95"},
- {file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"},
- {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"},
- {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"},
- {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"},
- {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"},
- {file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"},
- {file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"},
- {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"},
- {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"},
- {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"},
- {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"},
- {file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"},
- {file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"},
- {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"},
- {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"},
- {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"},
- {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"},
- {file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"},
- {file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"},
- {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"},
- {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"},
- {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"},
- {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"},
- {file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"},
- {file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"},
+ {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:364d03207f3e603922d0d3932ef363d55bbf48e3647395765f9bfcbdf6d23632"},
+ {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50127c186f191b8917ea2fb8b206fbebe87fd414a6084d15568c27d0a21d60db"},
+ {file = "lxml-5.2.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74e4f025ef3db1c6da4460dd27c118d8cd136d0391da4e387a15e48e5c975147"},
+ {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981a06a3076997adf7c743dcd0d7a0415582661e2517c7d961493572e909aa1d"},
+ {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aef5474d913d3b05e613906ba4090433c515e13ea49c837aca18bde190853dff"},
+ {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca"},
+ {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5b65529bb2f21ac7861a0e94fdbf5dc0daab41497d18223b46ee8515e5ad297"},
+ {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36"},
+ {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:b47633251727c8fe279f34025844b3b3a3e40cd1b198356d003aa146258d13a2"},
+ {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:fbc9d316552f9ef7bba39f4edfad4a734d3d6f93341232a9dddadec4f15d425f"},
+ {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:13e69be35391ce72712184f69000cda04fc89689429179bc4c0ae5f0b7a8c21b"},
+ {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3b6a30a9ab040b3f545b697cb3adbf3696c05a3a68aad172e3fd7ca73ab3c835"},
+ {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0"},
+ {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:dfa7c241073d8f2b8e8dbc7803c434f57dbb83ae2a3d7892dd068d99e96efe2c"},
+ {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316"},
+ {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0"},
+ {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ffb2be176fed4457e445fe540617f0252a72a8bc56208fd65a690fdb1f57660b"},
+ {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e290d79a4107d7d794634ce3e985b9ae4f920380a813717adf61804904dc4393"},
+ {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96e85aa09274955bb6bd483eaf5b12abadade01010478154b0ec70284c1b1526"},
+ {file = "lxml-5.2.2-cp310-cp310-win32.whl", hash = "sha256:f956196ef61369f1685d14dad80611488d8dc1ef00be57c0c5a03064005b0f30"},
+ {file = "lxml-5.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7"},
+ {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545"},
+ {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88"},
+ {file = "lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083"},
+ {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1"},
+ {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734"},
+ {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f"},
+ {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed"},
+ {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3"},
+ {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df"},
+ {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d"},
+ {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5"},
+ {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab"},
+ {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115"},
+ {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04"},
+ {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad"},
+ {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8"},
+ {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5"},
+ {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa"},
+ {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b"},
+ {file = "lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438"},
+ {file = "lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be"},
+ {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391"},
+ {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776"},
+ {file = "lxml-5.2.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2"},
+ {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5"},
+ {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97"},
+ {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6"},
+ {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18"},
+ {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85"},
+ {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73"},
+ {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466"},
+ {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927"},
+ {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf"},
+ {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf"},
+ {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67"},
+ {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d"},
+ {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585"},
+ {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe"},
+ {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c"},
+ {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836"},
+ {file = "lxml-5.2.2-cp312-cp312-win32.whl", hash = "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a"},
+ {file = "lxml-5.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48"},
+ {file = "lxml-5.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e3d9d13603410b72787579769469af730c38f2f25505573a5888a94b62b920f8"},
+ {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38b67afb0a06b8575948641c1d6d68e41b83a3abeae2ca9eed2ac59892b36706"},
+ {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573"},
+ {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:cf2a978c795b54c539f47964ec05e35c05bd045db5ca1e8366988c7f2fe6b3ce"},
+ {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:739e36ef7412b2bd940f75b278749106e6d025e40027c0b94a17ef7968d55d56"},
+ {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d8bbcd21769594dbba9c37d3c819e2d5847656ca99c747ddb31ac1701d0c0ed9"},
+ {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264"},
+ {file = "lxml-5.2.2-cp36-cp36m-win32.whl", hash = "sha256:02437fb7308386867c8b7b0e5bc4cd4b04548b1c5d089ffb8e7b31009b961dc3"},
+ {file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"},
+ {file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"},
+ {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"},
+ {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6"},
+ {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"},
+ {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c"},
+ {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"},
+ {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184"},
+ {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"},
+ {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f"},
+ {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"},
+ {file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"},
+ {file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"},
+ {file = "lxml-5.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3"},
+ {file = "lxml-5.2.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60fdd125d85bf9c279ffb8e94c78c51b3b6a37711464e1f5f31078b45002421"},
+ {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7e24cb69ee5f32e003f50e016d5fde438010c1022c96738b04fc2423e61706"},
+ {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23cfafd56887eaed93d07bc4547abd5e09d837a002b791e9767765492a75883f"},
+ {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5"},
+ {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48"},
+ {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e49b052b768bb74f58c7dda4e0bdf7b79d43a9204ca584ffe1fb48a6f3c84c66"},
+ {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d14a0d029a4e176795cef99c056d58067c06195e0c7e2dbb293bf95c08f772a3"},
+ {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b"},
+ {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1"},
+ {file = "lxml-5.2.2-cp38-cp38-win32.whl", hash = "sha256:5b8c041b6265e08eac8a724b74b655404070b636a8dd6d7a13c3adc07882ef30"},
+ {file = "lxml-5.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:f61efaf4bed1cc0860e567d2ecb2363974d414f7f1f124b1df368bbf183453a6"},
+ {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fb91819461b1b56d06fa4bcf86617fac795f6a99d12239fb0c68dbeba41a0a30"},
+ {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d4ed0c7cbecde7194cd3228c044e86bf73e30a23505af852857c09c24e77ec5d"},
+ {file = "lxml-5.2.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54401c77a63cc7d6dc4b4e173bb484f28a5607f3df71484709fe037c92d4f0ed"},
+ {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:625e3ef310e7fa3a761d48ca7ea1f9d8718a32b1542e727d584d82f4453d5eeb"},
+ {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:519895c99c815a1a24a926d5b60627ce5ea48e9f639a5cd328bda0515ea0f10c"},
+ {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001"},
+ {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:343ab62e9ca78094f2306aefed67dcfad61c4683f87eee48ff2fd74902447726"},
+ {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:cd9e78285da6c9ba2d5c769628f43ef66d96ac3085e59b10ad4f3707980710d3"},
+ {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:546cf886f6242dff9ec206331209db9c8e1643ae642dea5fdbecae2453cb50fd"},
+ {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:02f6a8eb6512fdc2fd4ca10a49c341c4e109aa6e9448cc4859af5b949622715a"},
+ {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:339ee4a4704bc724757cd5dd9dc8cf4d00980f5d3e6e06d5847c1b594ace68ab"},
+ {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0a028b61a2e357ace98b1615fc03f76eb517cc028993964fe08ad514b1e8892d"},
+ {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f90e552ecbad426eab352e7b2933091f2be77115bb16f09f78404861c8322981"},
+ {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d83e2d94b69bf31ead2fa45f0acdef0757fa0458a129734f59f67f3d2eb7ef32"},
+ {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a02d3c48f9bb1e10c7788d92c0c7db6f2002d024ab6e74d6f45ae33e3d0288a3"},
+ {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d68ce8e7b2075390e8ac1e1d3a99e8b6372c694bbe612632606d1d546794207"},
+ {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:453d037e09a5176d92ec0fd282e934ed26d806331a8b70ab431a81e2fbabf56d"},
+ {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3b019d4ee84b683342af793b56bb35034bd749e4cbdd3d33f7d1107790f8c472"},
+ {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9"},
+ {file = "lxml-5.2.2-cp39-cp39-win32.whl", hash = "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf"},
+ {file = "lxml-5.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:610b5c77428a50269f38a534057444c249976433f40f53e3b47e68349cca1425"},
+ {file = "lxml-5.2.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2"},
+ {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9"},
+ {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae"},
+ {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1"},
+ {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2"},
+ {file = "lxml-5.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b"},
+ {file = "lxml-5.2.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b"},
+ {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009"},
+ {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07"},
+ {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484"},
+ {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8"},
+ {file = "lxml-5.2.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b"},
+ {file = "lxml-5.2.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525"},
+ {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34"},
+ {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6"},
+ {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14"},
+ {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db"},
+ {file = "lxml-5.2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511"},
+ {file = "lxml-5.2.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d"},
+ {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0"},
+ {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e"},
+ {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182"},
+ {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a"},
+ {file = "lxml-5.2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324"},
+ {file = "lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87"},
]
[package.extras]
@@ -2217,39 +2161,40 @@ files = [
[[package]]
name = "matplotlib"
-version = "3.8.4"
+version = "3.9.0"
description = "Python plotting package"
optional = false
python-versions = ">=3.9"
files = [
- {file = "matplotlib-3.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014"},
- {file = "matplotlib-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106"},
- {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10"},
- {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0"},
- {file = "matplotlib-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef"},
- {file = "matplotlib-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338"},
- {file = "matplotlib-3.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661"},
- {file = "matplotlib-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c"},
- {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa"},
- {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71"},
- {file = "matplotlib-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b"},
- {file = "matplotlib-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae"},
- {file = "matplotlib-3.8.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616"},
- {file = "matplotlib-3.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732"},
- {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb"},
- {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30"},
- {file = "matplotlib-3.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25"},
- {file = "matplotlib-3.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a"},
- {file = "matplotlib-3.8.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:843cbde2f0946dadd8c5c11c6d91847abd18ec76859dc319362a0964493f0ba6"},
- {file = "matplotlib-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c13f041a7178f9780fb61cc3a2b10423d5e125480e4be51beaf62b172413b67"},
- {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb44f53af0a62dc80bba4443d9b27f2fde6acfdac281d95bc872dc148a6509cc"},
- {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9"},
- {file = "matplotlib-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9bb0189011785ea794ee827b68777db3ca3f93f3e339ea4d920315a0e5a78d54"},
- {file = "matplotlib-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:6209e5c9aaccc056e63b547a8152661324404dd92340a6e479b3a7f24b42a5d0"},
- {file = "matplotlib-3.8.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c7064120a59ce6f64103c9cefba8ffe6fba87f2c61d67c401186423c9a20fd35"},
- {file = "matplotlib-3.8.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0e47eda4eb2614300fc7bb4657fced3e83d6334d03da2173b09e447418d499f"},
- {file = "matplotlib-3.8.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:493e9f6aa5819156b58fce42b296ea31969f2aab71c5b680b4ea7a3cb5c07d94"},
- {file = "matplotlib-3.8.4.tar.gz", hash = "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea"},
+ {file = "matplotlib-3.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56"},
+ {file = "matplotlib-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b"},
+ {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"},
+ {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d"},
+ {file = "matplotlib-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4"},
+ {file = "matplotlib-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463"},
+ {file = "matplotlib-3.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38"},
+ {file = "matplotlib-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152"},
+ {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85"},
+ {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb"},
+ {file = "matplotlib-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674"},
+ {file = "matplotlib-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be"},
+ {file = "matplotlib-3.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382"},
+ {file = "matplotlib-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84"},
+ {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5"},
+ {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db"},
+ {file = "matplotlib-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7"},
+ {file = "matplotlib-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf"},
+ {file = "matplotlib-3.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956"},
+ {file = "matplotlib-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a"},
+ {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321"},
+ {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89"},
+ {file = "matplotlib-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b"},
+ {file = "matplotlib-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888"},
+ {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0"},
+ {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03"},
+ {file = "matplotlib-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd"},
+ {file = "matplotlib-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e"},
+ {file = "matplotlib-3.9.0.tar.gz", hash = "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a"},
]
[package.dependencies]
@@ -2257,21 +2202,24 @@ contourpy = ">=1.0.1"
cycler = ">=0.10"
fonttools = ">=4.22.0"
kiwisolver = ">=1.3.1"
-numpy = ">=1.21"
+numpy = ">=1.23"
packaging = ">=20.0"
pillow = ">=8"
pyparsing = ">=2.3.1"
python-dateutil = ">=2.7"
+[package.extras]
+dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"]
+
[[package]]
name = "mdit-py-plugins"
-version = "0.4.0"
+version = "0.4.1"
description = "Collection of plugins for markdown-it-py"
optional = false
python-versions = ">=3.8"
files = [
- {file = "mdit_py_plugins-0.4.0-py3-none-any.whl", hash = "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9"},
- {file = "mdit_py_plugins-0.4.0.tar.gz", hash = "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"},
+ {file = "mdit_py_plugins-0.4.1-py3-none-any.whl", hash = "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a"},
+ {file = "mdit_py_plugins-0.4.1.tar.gz", hash = "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c"},
]
[package.dependencies]
@@ -2299,18 +2247,16 @@ version = "0.4.2.3"
description = "An open-ended driving simulator with infinite scenes"
optional = false
python-versions = ">=3.6, <3.12"
-files = [
- {file = "metadrive-simulator-0.4.2.3.tar.gz", hash = "sha256:bcda7d07146128161b0bc2cc337e01612b9222202706370043b52f7936a8a277"},
- {file = "metadrive_simulator-0.4.2.3-py3-none-any.whl", hash = "sha256:f6fff20b931bb956c55e0e81bf1f6b641169a84a4c853435a8b26bd51c8e9876"},
-]
+files = []
+develop = false
[package.dependencies]
filelock = "*"
geopandas = "*"
-gymnasium = ">=0.28,<0.29"
+gymnasium = ">=0.28"
lxml = "*"
matplotlib = "*"
-numpy = ">=1.21.6,<=1.24.2"
+numpy = ">=1.21.6"
opencv-python = "*"
panda3d = "1.10.13"
panda3d-gltf = "0.13"
@@ -2333,6 +2279,12 @@ cuda = ["PyOpenGL (==3.1.6)", "PyOpenGL-accelerate (==3.1.6)", "cuda-python (==1
gym = ["gym (>=0.19.0,<=0.26.0)"]
ros = ["zmq"]
+[package.source]
+type = "git"
+url = "https://github.com/metadriverse/metadrive.git"
+reference = "233a3a1698be7038ec3dd050ca10b547b4b3324c"
+resolved_reference = "233a3a1698be7038ec3dd050ca10b547b4b3324c"
+
[[package]]
name = "mouseinfo"
version = "0.1.3"
@@ -2668,42 +2620,37 @@ files = [
[[package]]
name = "onnx"
-version = "1.16.0"
+version = "1.16.1"
description = "Open Neural Network Exchange"
optional = false
python-versions = ">=3.8"
files = [
- {file = "onnx-1.16.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:9eadbdce25b19d6216f426d6d99b8bc877a65ed92cbef9707751c6669190ba4f"},
- {file = "onnx-1.16.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:034ae21a2aaa2e9c14119a840d2926d213c27aad29e5e3edaa30145a745048e1"},
- {file = "onnx-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec22a43d74eb1f2303373e2fbe7fbcaa45fb225f4eb146edfed1356ada7a9aea"},
- {file = "onnx-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:298f28a2b5ac09145fa958513d3d1e6b349ccf86a877dbdcccad57713fe360b3"},
- {file = "onnx-1.16.0-cp310-cp310-win32.whl", hash = "sha256:66300197b52beca08bc6262d43c103289c5d45fde43fb51922ed1eb83658cf0c"},
- {file = "onnx-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae0029f5e47bf70a1a62e7f88c80bca4ef39b844a89910039184221775df5e43"},
- {file = "onnx-1.16.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:f51179d4af3372b4f3800c558d204b592c61e4b4a18b8f61e0eea7f46211221a"},
- {file = "onnx-1.16.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5202559070afec5144332db216c20f2fff8323cf7f6512b0ca11b215eacc5bf3"},
- {file = "onnx-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77579e7c15b4df39d29465b216639a5f9b74026bdd9e4b6306cd19a32dcfe67c"},
- {file = "onnx-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e60ca76ac24b65c25860d0f2d2cdd96d6320d062a01dd8ce87c5743603789b8"},
- {file = "onnx-1.16.0-cp311-cp311-win32.whl", hash = "sha256:81b4ee01bc554e8a2b11ac6439882508a5377a1c6b452acd69a1eebb83571117"},
- {file = "onnx-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:7449241e70b847b9c3eb8dae622df8c1b456d11032a9d7e26e0ee8a698d5bf86"},
- {file = "onnx-1.16.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:03a627488b1a9975d95d6a55582af3e14c7f3bb87444725b999935ddd271d352"},
- {file = "onnx-1.16.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c392faeabd9283ee344ccb4b067d1fea9dfc614fa1f0de7c47589efd79e15e78"},
- {file = "onnx-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0efeb46985de08f0efe758cb54ad3457e821a05c2eaf5ba2ccb8cd1602c08084"},
- {file = "onnx-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddf14a3d32234f23e44abb73a755cb96a423fac7f004e8f046f36b10214151ee"},
- {file = "onnx-1.16.0-cp312-cp312-win32.whl", hash = "sha256:62a2e27ae8ba5fc9b4a2620301446a517b5ffaaf8566611de7a7c2160f5bcf4c"},
- {file = "onnx-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:3e0860fea94efde777e81a6f68f65761ed5e5f3adea2e050d7fbe373a9ae05b3"},
- {file = "onnx-1.16.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:70a90649318f3470985439ea078277c9fb2a2e6e2fd7c8f3f2b279402ad6c7e6"},
- {file = "onnx-1.16.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:71839546b7f93be4fa807995b182ab4b4414c9dbf049fee11eaaced16fcf8df2"},
- {file = "onnx-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7665217c45a61eb44718c8e9349d2ad004efa0cb9fbc4be5c6d5e18b9fe12b52"},
- {file = "onnx-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5752bbbd5717304a7643643dba383a2fb31e8eb0682f4e7b7d141206328a73b"},
- {file = "onnx-1.16.0-cp38-cp38-win32.whl", hash = "sha256:257858cbcb2055284f09fa2ae2b1cfd64f5850367da388d6e7e7b05920a40c90"},
- {file = "onnx-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:209fe84995a28038e29ae8369edd35f33e0ef1ebc3bddbf6584629823469deb1"},
- {file = "onnx-1.16.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:8cf3e518b1b1b960be542e7c62bed4e5219e04c85d540817b7027029537dec92"},
- {file = "onnx-1.16.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:30f02beaf081c7d9fa3a8c566a912fc4408e28fc33b1452d58f890851691d364"},
- {file = "onnx-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fb29a9a692b522deef1f6b8f2145da62c0c43ea1ed5b4c0f66f827fdc28847d"},
- {file = "onnx-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7755cbd5f4e47952e37276ea5978a46fc8346684392315902b5ed4a719d87d06"},
- {file = "onnx-1.16.0-cp39-cp39-win32.whl", hash = "sha256:7532343dc5b8b5e7c3e3efa441a3100552f7600155c4db9120acd7574f64ffbf"},
- {file = "onnx-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:d7886c05aa6d583ec42f6287678923c1e343afc4350e49d5b36a0023772ffa22"},
- {file = "onnx-1.16.0.tar.gz", hash = "sha256:237c6987c6c59d9f44b6136f5819af79574f8d96a760a1fa843bede11f3822f7"},
+ {file = "onnx-1.16.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:bb2d392e5b7060082c2fb38eb5c44f67eb34ff5f0681bd6f45beff9abc6f7094"},
+ {file = "onnx-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15abf94a7868eed6db15a8b5024ba570c891cae77ca4d0e7258dabdad76980df"},
+ {file = "onnx-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6251910e554f811fdd070164b0bc76d76b067b95576cb9dad4d52ae64fe014b5"},
+ {file = "onnx-1.16.1-cp310-cp310-win32.whl", hash = "sha256:c11e3b15eee46cd20767e505cc3ba97457ef5ac93c3e459cdfb77943ff8fe9a7"},
+ {file = "onnx-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:b3d10405706807ec2ef493b2a78519fa0264cf190363e89478585aac1179b596"},
+ {file = "onnx-1.16.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:006ba5059c85ce43e89a1486cc0276d0f1a8ec9c6efd1a9334fd3fa0f6e33b64"},
+ {file = "onnx-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1521ea7cd3497ecaf57d3b5e72d637ca5ebca632122a0806a9df99bedbeecdf8"},
+ {file = "onnx-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45cf20421aeac03872bea5fd6ebf92abe15c4d1461a2572eb839add5059e2a09"},
+ {file = "onnx-1.16.1-cp311-cp311-win32.whl", hash = "sha256:f98e275b4f46a617a9c527e60c02531eae03cf67a04c26db8a1c20acee539533"},
+ {file = "onnx-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:95aa20aa65a9035d7543e81713e8b0f611e213fc02171959ef4ee09311d1bf28"},
+ {file = "onnx-1.16.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:32e11d39bee04f927fab09f74c46cf76584094462311bab1aca9ccdae6ed3366"},
+ {file = "onnx-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8884bf53b552873c0c9b072cb8625e7d4e8f3cc0529191632d24e3de58a3b93a"},
+ {file = "onnx-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595b2830093f81361961295f7b0ebb6000423bcd04123d516d081c306002e387"},
+ {file = "onnx-1.16.1-cp312-cp312-win32.whl", hash = "sha256:2fde4dd5bc278b3fc8148f460bce8807b2874c66f48529df9444cdbc9ecf456b"},
+ {file = "onnx-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:e69ad8c110d8c37d759cad019d498fdf3fd24e0bfaeb960e52fed0469a5d2974"},
+ {file = "onnx-1.16.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:0fc189195a40b5862fb77d97410c89823197fe19c1088ce150444eec72f200c1"},
+ {file = "onnx-1.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496ba17b16a74711081772e1b03f3207959972e351298e51abdc600051027a22"},
+ {file = "onnx-1.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3faf239b48418b3ea6fe73bd4d86807b903d0b2ebd20b8b8c84f83741b0f18"},
+ {file = "onnx-1.16.1-cp38-cp38-win32.whl", hash = "sha256:18b22143836838591f6551b089196e69f60c47fabce52b4b72b4cb37522645aa"},
+ {file = "onnx-1.16.1-cp38-cp38-win_amd64.whl", hash = "sha256:8c2b70d602acfb90056fbdc60ef26f4658f964591212a4e9dbbda922ff43061b"},
+ {file = "onnx-1.16.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:2bed6fe05905b073206cabbb4463c58050cf8d544192303c09927b229f93ac14"},
+ {file = "onnx-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5798414332534a41404a7ff83677d49ced01d70160e1541484cce647f2295051"},
+ {file = "onnx-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa7518d6d27f357261a4014079dec364cad6fef827d0b3fe1d3ff59939a68394"},
+ {file = "onnx-1.16.1-cp39-cp39-win32.whl", hash = "sha256:67f372db4fe8fe61e00b762af5b0833aa72b5baa37e7e2f47d8668964ebff411"},
+ {file = "onnx-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c059fea6229c44d2d39c8f6e2f2f0d676d587c97f4c854c86f3e7bc97e0b31c"},
+ {file = "onnx-1.16.1.tar.gz", hash = "sha256:8299193f0f2a3849bfc069641aa8e4f93696602da8d165632af8ee48ec7556b6"},
]
[package.dependencies]
@@ -2715,36 +2662,36 @@ reference = ["Pillow", "google-re2"]
[[package]]
name = "onnxruntime"
-version = "1.17.3"
+version = "1.18.0"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false
python-versions = "*"
files = [
- {file = "onnxruntime-1.17.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d86dde9c0bb435d709e51bd25991c9fe5b9a5b168df45ce119769edc4d198b15"},
- {file = "onnxruntime-1.17.3-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d87b68bf931ac527b2d3c094ead66bb4381bac4298b65f46c54fe4d1e255865"},
- {file = "onnxruntime-1.17.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e950cf0333cf114a155f9142e71da344d2b08dfe202763a403ae81cc02ebd1"},
- {file = "onnxruntime-1.17.3-cp310-cp310-win32.whl", hash = "sha256:0962a4d0f5acebf62e1f0bf69b6e0adf16649115d8de854c1460e79972324d68"},
- {file = "onnxruntime-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:468ccb8a0faa25c681a41787b1594bf4448b0252d3efc8b62fd8b2411754340f"},
- {file = "onnxruntime-1.17.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e8cd90c1c17d13d47b89ab076471e07fb85467c01dcd87a8b8b5cdfbcb40aa51"},
- {file = "onnxruntime-1.17.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a058b39801baefe454eeb8acf3ada298c55a06a4896fafc224c02d79e9037f60"},
- {file = "onnxruntime-1.17.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f823d5eb4807007f3da7b27ca972263df6a1836e6f327384eb266274c53d05d"},
- {file = "onnxruntime-1.17.3-cp311-cp311-win32.whl", hash = "sha256:b66b23f9109e78ff2791628627a26f65cd335dcc5fbd67ff60162733a2f7aded"},
- {file = "onnxruntime-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:570760ca53a74cdd751ee49f13de70d1384dcf73d9888b8deac0917023ccda6d"},
- {file = "onnxruntime-1.17.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:77c318178d9c16e9beadd9a4070d8aaa9f57382c3f509b01709f0f010e583b99"},
- {file = "onnxruntime-1.17.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23da8469049b9759082e22c41a444f44a520a9c874b084711b6343672879f50b"},
- {file = "onnxruntime-1.17.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2949730215af3f9289008b2e31e9bbef952012a77035b911c4977edea06f3f9e"},
- {file = "onnxruntime-1.17.3-cp312-cp312-win32.whl", hash = "sha256:6c7555a49008f403fb3b19204671efb94187c5085976ae526cb625f6ede317bc"},
- {file = "onnxruntime-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:58672cf20293a1b8a277a5c6c55383359fcdf6119b2f14df6ce3b140f5001c39"},
- {file = "onnxruntime-1.17.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:4395ba86e3c1e93c794a00619ef1aec597ab78f5a5039f3c6d2e9d0695c0a734"},
- {file = "onnxruntime-1.17.3-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf354c04344ec38564fc22394e1fe08aa6d70d790df00159205a0055c4a4d3f"},
- {file = "onnxruntime-1.17.3-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a94b600b7af50e922d44b95a57981e3e35103c6e3693241a03d3ca204740bbda"},
- {file = "onnxruntime-1.17.3-cp38-cp38-win32.whl", hash = "sha256:5a335c76f9c002a8586c7f38bc20fe4b3725ced21f8ead835c3e4e507e42b2ab"},
- {file = "onnxruntime-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f56a86fbd0ddc8f22696ddeda0677b041381f4168a2ca06f712ef6ec6050d6d"},
- {file = "onnxruntime-1.17.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:e0ae39f5452278cd349520c296e7de3e90d62dc5b0157c6868e2748d7f28b871"},
- {file = "onnxruntime-1.17.3-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ff2dc012bd930578aff5232afd2905bf16620815f36783a941aafabf94b3702"},
- {file = "onnxruntime-1.17.3-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf6c37483782e4785019b56e26224a25e9b9a35b849d0169ce69189867a22bb1"},
- {file = "onnxruntime-1.17.3-cp39-cp39-win32.whl", hash = "sha256:351bf5a1140dcc43bfb8d3d1a230928ee61fcd54b0ea664c8e9a889a8e3aa515"},
- {file = "onnxruntime-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:57a3de15778da8d6cc43fbf6cf038e1e746146300b5f0b1fbf01f6f795dc6440"},
+ {file = "onnxruntime-1.18.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:5a3b7993a5ecf4a90f35542a4757e29b2d653da3efe06cdd3164b91167bbe10d"},
+ {file = "onnxruntime-1.18.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15b944623b2cdfe7f7945690bfb71c10a4531b51997c8320b84e7b0bb59af902"},
+ {file = "onnxruntime-1.18.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e61ce5005118064b1a0ed73ebe936bc773a102f067db34108ea6c64dd62a179"},
+ {file = "onnxruntime-1.18.0-cp310-cp310-win32.whl", hash = "sha256:a4fc8a2a526eb442317d280610936a9f73deece06c7d5a91e51570860802b93f"},
+ {file = "onnxruntime-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:71ed219b768cab004e5cd83e702590734f968679bf93aa488c1a7ffbe6e220c3"},
+ {file = "onnxruntime-1.18.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:3d24bd623872a72a7fe2f51c103e20fcca2acfa35d48f2accd6be1ec8633d960"},
+ {file = "onnxruntime-1.18.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f15e41ca9b307a12550bfd2ec93f88905d9fba12bab7e578f05138ad0ae10d7b"},
+ {file = "onnxruntime-1.18.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f45ca2887f62a7b847d526965686b2923efa72538c89b7703c7b3fe970afd59"},
+ {file = "onnxruntime-1.18.0-cp311-cp311-win32.whl", hash = "sha256:9e24d9ecc8781323d9e2eeda019b4b24babc4d624e7d53f61b1fe1a929b0511a"},
+ {file = "onnxruntime-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:f8608398976ed18aef450d83777ff6f77d0b64eced1ed07a985e1a7db8ea3771"},
+ {file = "onnxruntime-1.18.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f1d79941f15fc40b1ee67738b2ca26b23e0181bf0070b5fb2984f0988734698f"},
+ {file = "onnxruntime-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e8caf3a8565c853a22d323a3eebc2a81e3de7591981f085a4f74f7a60aab2d"},
+ {file = "onnxruntime-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:498d2b8380635f5e6ebc50ec1b45f181588927280f32390fb910301d234f97b8"},
+ {file = "onnxruntime-1.18.0-cp312-cp312-win32.whl", hash = "sha256:ba7cc0ce2798a386c082aaa6289ff7e9bedc3dee622eef10e74830cff200a72e"},
+ {file = "onnxruntime-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:1fa175bd43f610465d5787ae06050c81f7ce09da2bf3e914eb282cb8eab363ef"},
+ {file = "onnxruntime-1.18.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:0284c579c20ec8b1b472dd190290a040cc68b6caec790edb960f065d15cf164a"},
+ {file = "onnxruntime-1.18.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d47353d036d8c380558a5643ea5f7964d9d259d31c86865bad9162c3e916d1f6"},
+ {file = "onnxruntime-1.18.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:885509d2b9ba4b01f08f7fa28d31ee54b6477953451c7ccf124a84625f07c803"},
+ {file = "onnxruntime-1.18.0-cp38-cp38-win32.whl", hash = "sha256:8614733de3695656411d71fc2f39333170df5da6c7efd6072a59962c0bc7055c"},
+ {file = "onnxruntime-1.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:47af3f803752fce23ea790fd8d130a47b2b940629f03193f780818622e856e7a"},
+ {file = "onnxruntime-1.18.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:9153eb2b4d5bbab764d0aea17adadffcfc18d89b957ad191b1c3650b9930c59f"},
+ {file = "onnxruntime-1.18.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c7fd86eca727c989bb8d9c5104f3c45f7ee45f445cc75579ebe55d6b99dfd7c"},
+ {file = "onnxruntime-1.18.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac67a4de9c1326c4d87bcbfb652c923039b8a2446bb28516219236bec3b494f5"},
+ {file = "onnxruntime-1.18.0-cp39-cp39-win32.whl", hash = "sha256:6ffb445816d06497df7a6dd424b20e0b2c39639e01e7fe210e247b82d15a23b9"},
+ {file = "onnxruntime-1.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:46de6031cb6745f33f7eca9e51ab73e8c66037fb7a3b6b4560887c5b55ab5d5d"},
]
[package.dependencies]
@@ -2757,21 +2704,21 @@ sympy = "*"
[[package]]
name = "onnxruntime-gpu"
-version = "1.17.1"
+version = "1.18.0"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false
python-versions = "*"
files = [
- {file = "onnxruntime_gpu-1.17.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a55fe84ee11a59ea069c6a790ee960f1c7da0d7d6c74822b2a8b357027c93646"},
- {file = "onnxruntime_gpu-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:a9abefceb32879cbee9f57977d6bb8d58cbac501f8a64bf96bca2f4fdff157fe"},
- {file = "onnxruntime_gpu-1.17.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b2cd54f2b0a05e6bc9ab30182b859364d30115a19c31be24aa2edef40be00277"},
- {file = "onnxruntime_gpu-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdffcced8a5f6275c0df202220e9232138b336f868cd671c9d2c571e834d2a80"},
- {file = "onnxruntime_gpu-1.17.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a1c871e8d0ae4121ea6528fc9410a5a7cbc5e43714b30521d5514fd10b987c83"},
- {file = "onnxruntime_gpu-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:9a0a94eda080e9f4a8e5035fdf0b3c24f5533e7861d88833a94493e63fca0812"},
- {file = "onnxruntime_gpu-1.17.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:624fdb65a632833f13de36854855818680be4f77942d8114524491d58f60d3ab"},
- {file = "onnxruntime_gpu-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:29fa78d232bbb5a5be3a3e0a022148a7b3df2ca66b4c21a11eef56e6f22859e9"},
- {file = "onnxruntime_gpu-1.17.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b0f8c70f2f9aeae825f3a397cc0c5f45124f9ae7c173263cf13c495982b0b99a"},
- {file = "onnxruntime_gpu-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:b1a27a104334461b690e4fc62775e1e71c68936399874932225d7fea21a0c261"},
+ {file = "onnxruntime_gpu-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ebee59886d4a63d54cec673c73bdcc58b5496f9946e3c539af51550c6f222e88"},
+ {file = "onnxruntime_gpu-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:c33a2bfd100bd2b82542c72deaaed40d3de858ac9624dc5730e548869dac2f2f"},
+ {file = "onnxruntime_gpu-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:aa673c044f450b21163265cca2e35eb1ded03d48fb01dec6be1c412811e9b2b0"},
+ {file = "onnxruntime_gpu-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:1dfb1172de0043f7bdc32724619f5bc151dbdc5bb8e50e806271d2efa8d72715"},
+ {file = "onnxruntime_gpu-1.18.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:40ce4aec8499352b96f8c67d1f2fe4ac170dbc9281dd8ebe36339d9e1c0bcdb7"},
+ {file = "onnxruntime_gpu-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:97df451777322a534dbedff49d43a4f6f74bd5258ccf31b2161e1af606bc724d"},
+ {file = "onnxruntime_gpu-1.18.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:05b5f17bc78586741715903b5ec7fcef4c64d136aee94d6583dd17035190d550"},
+ {file = "onnxruntime_gpu-1.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:40fab33312d02fdaa93b8cc14ffb1fc3aceaef5db50383a579fd8b7ad5a03b17"},
+ {file = "onnxruntime_gpu-1.18.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e0d9e200afe57c69c6407c90189c7f16a55ce326d1ac45864353e13bfe574b3c"},
+ {file = "onnxruntime_gpu-1.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:a7d13f0983eec46857acab8bdace7e3178428d17ad3c6d5b84b5b211c98e5acd"},
]
[package.dependencies]
@@ -3120,13 +3067,13 @@ xmp = ["defusedxml"]
[[package]]
name = "platformdirs"
-version = "4.2.1"
+version = "4.2.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
- {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"},
- {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"},
+ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
+ {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
]
[package.extras]
@@ -3195,13 +3142,13 @@ files = [
[[package]]
name = "pre-commit"
-version = "3.7.0"
+version = "3.7.1"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.9"
files = [
- {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"},
- {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"},
+ {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"},
+ {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"},
]
[package.dependencies]
@@ -3223,22 +3170,22 @@ files = [
[[package]]
name = "protobuf"
-version = "5.26.1"
+version = "5.27.0"
description = ""
optional = false
python-versions = ">=3.8"
files = [
- {file = "protobuf-5.26.1-cp310-abi3-win32.whl", hash = "sha256:3c388ea6ddfe735f8cf69e3f7dc7611e73107b60bdfcf5d0f024c3ccd3794e23"},
- {file = "protobuf-5.26.1-cp310-abi3-win_amd64.whl", hash = "sha256:e6039957449cb918f331d32ffafa8eb9255769c96aa0560d9a5bf0b4e00a2a33"},
- {file = "protobuf-5.26.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:38aa5f535721d5bb99861166c445c4105c4e285c765fbb2ac10f116e32dcd46d"},
- {file = "protobuf-5.26.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:fbfe61e7ee8c1860855696e3ac6cfd1b01af5498facc6834fcc345c9684fb2ca"},
- {file = "protobuf-5.26.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:f7417703f841167e5a27d48be13389d52ad705ec09eade63dfc3180a959215d7"},
- {file = "protobuf-5.26.1-cp38-cp38-win32.whl", hash = "sha256:d693d2504ca96750d92d9de8a103102dd648fda04540495535f0fec7577ed8fc"},
- {file = "protobuf-5.26.1-cp38-cp38-win_amd64.whl", hash = "sha256:9b557c317ebe6836835ec4ef74ec3e994ad0894ea424314ad3552bc6e8835b4e"},
- {file = "protobuf-5.26.1-cp39-cp39-win32.whl", hash = "sha256:b9ba3ca83c2e31219ffbeb9d76b63aad35a3eb1544170c55336993d7a18ae72c"},
- {file = "protobuf-5.26.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ee014c2c87582e101d6b54260af03b6596728505c79f17c8586e7523aaa8f8c"},
- {file = "protobuf-5.26.1-py3-none-any.whl", hash = "sha256:da612f2720c0183417194eeaa2523215c4fcc1a1949772dc65f05047e08d5932"},
- {file = "protobuf-5.26.1.tar.gz", hash = "sha256:8ca2a1d97c290ec7b16e4e5dff2e5ae150cc1582f55b5ab300d45cb0dfa90e51"},
+ {file = "protobuf-5.27.0-cp310-abi3-win32.whl", hash = "sha256:2f83bf341d925650d550b8932b71763321d782529ac0eaf278f5242f513cc04e"},
+ {file = "protobuf-5.27.0-cp310-abi3-win_amd64.whl", hash = "sha256:b276e3f477ea1eebff3c2e1515136cfcff5ac14519c45f9b4aa2f6a87ea627c4"},
+ {file = "protobuf-5.27.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:744489f77c29174328d32f8921566fb0f7080a2f064c5137b9d6f4b790f9e0c1"},
+ {file = "protobuf-5.27.0-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:f51f33d305e18646f03acfdb343aac15b8115235af98bc9f844bf9446573827b"},
+ {file = "protobuf-5.27.0-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:56937f97ae0dcf4e220ff2abb1456c51a334144c9960b23597f044ce99c29c89"},
+ {file = "protobuf-5.27.0-cp38-cp38-win32.whl", hash = "sha256:a17f4d664ea868102feaa30a674542255f9f4bf835d943d588440d1f49a3ed15"},
+ {file = "protobuf-5.27.0-cp38-cp38-win_amd64.whl", hash = "sha256:aabbbcf794fbb4c692ff14ce06780a66d04758435717107c387f12fb477bf0d8"},
+ {file = "protobuf-5.27.0-cp39-cp39-win32.whl", hash = "sha256:587be23f1212da7a14a6c65fd61995f8ef35779d4aea9e36aad81f5f3b80aec5"},
+ {file = "protobuf-5.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cb65fc8fba680b27cf7a07678084c6e68ee13cab7cace734954c25a43da6d0f"},
+ {file = "protobuf-5.27.0-py3-none-any.whl", hash = "sha256:673ad60f1536b394b4fa0bcd3146a4130fcad85bfe3b60eaa86d6a0ace0fa374"},
+ {file = "protobuf-5.27.0.tar.gz", hash = "sha256:07f2b9a15255e3cf3f137d884af7972407b556a7a220912b252f26dc3121e6bf"},
]
[[package]]
@@ -3269,6 +3216,54 @@ files = [
[package.extras]
test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
+[[package]]
+name = "pyarrow"
+version = "16.1.0"
+description = "Python library for Apache Arrow"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pyarrow-16.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:17e23b9a65a70cc733d8b738baa6ad3722298fa0c81d88f63ff94bf25eaa77b9"},
+ {file = "pyarrow-16.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4740cc41e2ba5d641071d0ab5e9ef9b5e6e8c7611351a5cb7c1d175eaf43674a"},
+ {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98100e0268d04e0eec47b73f20b39c45b4006f3c4233719c3848aa27a03c1aef"},
+ {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68f409e7b283c085f2da014f9ef81e885d90dcd733bd648cfba3ef265961848"},
+ {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a8914cd176f448e09746037b0c6b3a9d7688cef451ec5735094055116857580c"},
+ {file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:48be160782c0556156d91adbdd5a4a7e719f8d407cb46ae3bb4eaee09b3111bd"},
+ {file = "pyarrow-16.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cf389d444b0f41d9fe1444b70650fea31e9d52cfcb5f818b7888b91b586efff"},
+ {file = "pyarrow-16.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:d0ebea336b535b37eee9eee31761813086d33ed06de9ab6fc6aaa0bace7b250c"},
+ {file = "pyarrow-16.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e73cfc4a99e796727919c5541c65bb88b973377501e39b9842ea71401ca6c1c"},
+ {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9251264247ecfe93e5f5a0cd43b8ae834f1e61d1abca22da55b20c788417f6"},
+ {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddf5aace92d520d3d2a20031d8b0ec27b4395cab9f74e07cc95edf42a5cc0147"},
+ {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:25233642583bf658f629eb230b9bb79d9af4d9f9229890b3c878699c82f7d11e"},
+ {file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a33a64576fddfbec0a44112eaf844c20853647ca833e9a647bfae0582b2ff94b"},
+ {file = "pyarrow-16.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:185d121b50836379fe012753cf15c4ba9638bda9645183ab36246923875f8d1b"},
+ {file = "pyarrow-16.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:2e51ca1d6ed7f2e9d5c3c83decf27b0d17bb207a7dea986e8dc3e24f80ff7d6f"},
+ {file = "pyarrow-16.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06ebccb6f8cb7357de85f60d5da50e83507954af617d7b05f48af1621d331c9a"},
+ {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b04707f1979815f5e49824ce52d1dceb46e2f12909a48a6a753fe7cafbc44a0c"},
+ {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d32000693deff8dc5df444b032b5985a48592c0697cb6e3071a5d59888714e2"},
+ {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8785bb10d5d6fd5e15d718ee1d1f914fe768bf8b4d1e5e9bf253de8a26cb1628"},
+ {file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e1369af39587b794873b8a307cc6623a3b1194e69399af0efd05bb202195a5a7"},
+ {file = "pyarrow-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:febde33305f1498f6df85e8020bca496d0e9ebf2093bab9e0f65e2b4ae2b3444"},
+ {file = "pyarrow-16.1.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b5f5705ab977947a43ac83b52ade3b881eb6e95fcc02d76f501d549a210ba77f"},
+ {file = "pyarrow-16.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0d27bf89dfc2576f6206e9cd6cf7a107c9c06dc13d53bbc25b0bd4556f19cf5f"},
+ {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d07de3ee730647a600037bc1d7b7994067ed64d0eba797ac74b2bc77384f4c2"},
+ {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbef391b63f708e103df99fbaa3acf9f671d77a183a07546ba2f2c297b361e83"},
+ {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19741c4dbbbc986d38856ee7ddfdd6a00fc3b0fc2d928795b95410d38bb97d15"},
+ {file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f2c5fb249caa17b94e2b9278b36a05ce03d3180e6da0c4c3b3ce5b2788f30eed"},
+ {file = "pyarrow-16.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:e6b6d3cd35fbb93b70ade1336022cc1147b95ec6af7d36906ca7fe432eb09710"},
+ {file = "pyarrow-16.1.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:18da9b76a36a954665ccca8aa6bd9f46c1145f79c0bb8f4f244f5f8e799bca55"},
+ {file = "pyarrow-16.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:99f7549779b6e434467d2aa43ab2b7224dd9e41bdde486020bae198978c9e05e"},
+ {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f07fdffe4fd5b15f5ec15c8b64584868d063bc22b86b46c9695624ca3505b7b4"},
+ {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddfe389a08ea374972bd4065d5f25d14e36b43ebc22fc75f7b951f24378bf0b5"},
+ {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3b20bd67c94b3a2ea0a749d2a5712fc845a69cb5d52e78e6449bbd295611f3aa"},
+ {file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ba8ac20693c0bb0bf4b238751d4409e62852004a8cf031c73b0e0962b03e45e3"},
+ {file = "pyarrow-16.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:31a1851751433d89a986616015841977e0a188662fcffd1a5677453f1df2de0a"},
+ {file = "pyarrow-16.1.0.tar.gz", hash = "sha256:15fbb22ea96d11f0b5768504a3f961edab25eaf4197c341720c4a387f6c60315"},
+]
+
+[package.dependencies]
+numpy = ">=1.16.6"
+
[[package]]
name = "pyaudio"
version = "0.2.14"
@@ -6639,13 +6634,13 @@ cp2110 = ["hidapi"]
[[package]]
name = "pytest"
-version = "8.2.0"
+version = "8.2.1"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"},
- {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"},
+ {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"},
+ {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"},
]
[package.dependencies]
@@ -6657,6 +6652,24 @@ pluggy = ">=1.5,<2.0"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+[[package]]
+name = "pytest-asyncio"
+version = "0.23.7"
+description = "Pytest support for asyncio"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
+ {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0,<9"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
+
[[package]]
name = "pytest-cov"
version = "5.0.0"
@@ -6690,6 +6703,23 @@ files = [
colorama = "*"
pytest = ">=7.0"
+[[package]]
+name = "pytest-mock"
+version = "3.14.0"
+description = "Thin-wrapper around the mock package for easier use with pytest"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
+ {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
+]
+
+[package.dependencies]
+pytest = ">=6.2.5"
+
+[package.extras]
+dev = ["pre-commit", "pytest-asyncio", "tox"]
+
[[package]]
name = "pytest-randomly"
version = "3.15.0"
@@ -6793,13 +6823,13 @@ files = [
[[package]]
name = "pytools"
-version = "2024.1.2"
+version = "2024.1.3"
description = "A collection of tools for Python"
optional = false
python-versions = "~=3.8"
files = [
- {file = "pytools-2024.1.2-py2.py3-none-any.whl", hash = "sha256:f61287b5341e53e3fe96c82385469b57a8983ff3db815a2bf3f533c38e8d516b"},
- {file = "pytools-2024.1.2.tar.gz", hash = "sha256:081871e451505c4b986ebafa68aeeabfdc7beb3faa1baa50f726aebe21e1d057"},
+ {file = "pytools-2024.1.3-py2.py3-none-any.whl", hash = "sha256:2977a7e9580fac6260d1352ed7a69a12a0c59687f834e0ebaecaf1148eceb7f3"},
+ {file = "pytools-2024.1.3.tar.gz", hash = "sha256:cc2db25666aa64094d3fb4532aa8a7deaa2da8edd7340fb270ed1807dcc75202"},
]
[package.dependencies]
@@ -7056,13 +7086,13 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""}
[[package]]
name = "requests"
-version = "2.31.0"
+version = "2.32.2"
description = "Python HTTP for Humans."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
- {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+ {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"},
+ {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"},
]
[package.dependencies]
@@ -7075,6 +7105,30 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+[[package]]
+name = "rerun-sdk"
+version = "0.16.0"
+description = "The Rerun Logging SDK"
+optional = false
+python-versions = "<3.13,>=3.8"
+files = [
+ {file = "rerun_sdk-0.16.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1cc6dc66d089e296f945dc238301889efb61dd6d338b5d00f76981cf7aed0a74"},
+ {file = "rerun_sdk-0.16.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:faf231897655e46eb975695df2b0ace07db362d697e697f9a3dff52f81c0dc5d"},
+ {file = "rerun_sdk-0.16.0-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:860a6394380d3e9b9e48bf34423bd56dda54d5b0158d2ae0e433698659b34198"},
+ {file = "rerun_sdk-0.16.0-cp38-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:5b8d1476f73a3ad1a5d3f21b61c633f3ab62aa80fa0b049f5ad10bf1227681ab"},
+ {file = "rerun_sdk-0.16.0-cp38-abi3-win_amd64.whl", hash = "sha256:aff0051a263b8c3067243c0126d319845baf4fe640899f04aeef7daf151f35e4"},
+]
+
+[package.dependencies]
+attrs = ">=23.1.0"
+numpy = ">=1.23,<2"
+pillow = ">=8.0.0"
+pyarrow = ">=14.0.2"
+typing-extensions = ">=4.5"
+
+[package.extras]
+tests = ["pytest (==7.1.2)"]
+
[[package]]
name = "rubicon-objc"
version = "0.4.9"
@@ -7092,62 +7146,62 @@ docs = ["furo (==2024.4.27)", "pyenchant (==3.2.2)", "sphinx (==7.1.2)", "sphinx
[[package]]
name = "ruff"
-version = "0.4.3"
+version = "0.4.5"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
- {file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"},
- {file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"},
- {file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"},
- {file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"},
- {file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"},
- {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"},
- {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"},
- {file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"},
- {file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"},
- {file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"},
- {file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"},
- {file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"},
- {file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"},
- {file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"},
- {file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"},
- {file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"},
- {file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"},
+ {file = "ruff-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"},
+ {file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"},
+ {file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"},
+ {file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"},
+ {file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"},
+ {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"},
+ {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"},
+ {file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"},
+ {file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"},
+ {file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"},
+ {file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"},
+ {file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"},
+ {file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"},
+ {file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"},
+ {file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"},
+ {file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"},
+ {file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"},
]
[[package]]
name = "scipy"
-version = "1.13.0"
+version = "1.13.1"
description = "Fundamental algorithms for scientific computing in Python"
optional = false
python-versions = ">=3.9"
files = [
- {file = "scipy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba419578ab343a4e0a77c0ef82f088238a93eef141b2b8017e46149776dfad4d"},
- {file = "scipy-1.13.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:22789b56a999265431c417d462e5b7f2b487e831ca7bef5edeb56efe4c93f86e"},
- {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f1432ba070e90d42d7fd836462c50bf98bd08bed0aa616c359eed8a04e3922"},
- {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8434f6f3fa49f631fae84afee424e2483289dfc30a47755b4b4e6b07b2633a4"},
- {file = "scipy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dcbb9ea49b0167de4167c40eeee6e167caeef11effb0670b554d10b1e693a8b9"},
- {file = "scipy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:1d2f7bb14c178f8b13ebae93f67e42b0a6b0fc50eba1cd8021c9b6e08e8fb1cd"},
- {file = "scipy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fbcf8abaf5aa2dc8d6400566c1a727aed338b5fe880cde64907596a89d576fa"},
- {file = "scipy-1.13.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5e4a756355522eb60fcd61f8372ac2549073c8788f6114449b37e9e8104f15a5"},
- {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5acd8e1dbd8dbe38d0004b1497019b2dbbc3d70691e65d69615f8a7292865d7"},
- {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d"},
- {file = "scipy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4dca18c3ffee287ddd3bc8f1dabaf45f5305c5afc9f8ab9cbfab855e70b2df5c"},
- {file = "scipy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a2f471de4d01200718b2b8927f7d76b5d9bde18047ea0fa8bd15c5ba3f26a1d6"},
- {file = "scipy-1.13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0de696f589681c2802f9090fff730c218f7c51ff49bf252b6a97ec4a5d19e8b"},
- {file = "scipy-1.13.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b2a3ff461ec4756b7e8e42e1c681077349a038f0686132d623fa404c0bee2551"},
- {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf9fe63e7a4bf01d3645b13ff2aa6dea023d38993f42aaac81a18b1bda7a82a"},
- {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42"},
- {file = "scipy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:109d391d720fcebf2fbe008621952b08e52907cf4c8c7efc7376822151820820"},
- {file = "scipy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8930ae3ea371d6b91c203b1032b9600d69c568e537b7988a3073dfe4d4774f21"},
- {file = "scipy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5407708195cb38d70fd2d6bb04b1b9dd5c92297d86e9f9daae1576bd9e06f602"},
- {file = "scipy-1.13.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ac38c4c92951ac0f729c4c48c9e13eb3675d9986cc0c83943784d7390d540c78"},
- {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c74543c4fbeb67af6ce457f6a6a28e5d3739a87f62412e4a16e46f164f0ae5"},
- {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d"},
- {file = "scipy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33fde20efc380bd23a78a4d26d59fc8704e9b5fd9b08841693eb46716ba13d86"},
- {file = "scipy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:45c08bec71d3546d606989ba6e7daa6f0992918171e2a6f7fbedfa7361c2de1e"},
- {file = "scipy-1.13.0.tar.gz", hash = "sha256:58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e"},
+ {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"},
+ {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"},
+ {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"},
+ {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"},
+ {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"},
+ {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"},
+ {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"},
+ {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"},
+ {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"},
+ {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"},
+ {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"},
+ {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"},
+ {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"},
+ {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"},
+ {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"},
+ {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"},
+ {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"},
+ {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"},
+ {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"},
+ {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"},
+ {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"},
+ {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"},
+ {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"},
+ {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"},
+ {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"},
]
[package.dependencies]
@@ -7192,13 +7246,13 @@ stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"]
[[package]]
name = "sentry-sdk"
-version = "2.1.1"
+version = "2.3.1"
description = "Python client for Sentry (https://sentry.io)"
optional = false
python-versions = ">=3.6"
files = [
- {file = "sentry_sdk-2.1.1-py2.py3-none-any.whl", hash = "sha256:99aeb78fb76771513bd3b2829d12613130152620768d00cd3e45ac00cb17950f"},
- {file = "sentry_sdk-2.1.1.tar.gz", hash = "sha256:95d8c0bb41c8b0bc37ab202c2c4a295bb84398ee05f4cdce55051cd75b926ec1"},
+ {file = "sentry_sdk-2.3.1-py2.py3-none-any.whl", hash = "sha256:c5aeb095ba226391d337dd42a6f9470d86c9fc236ecc71cfc7cd1942b45010c6"},
+ {file = "sentry_sdk-2.3.1.tar.gz", hash = "sha256:139a71a19f5e9eb5d3623942491ce03cf8ebc14ea2e39ba3e6fe79560d8a5b1f"},
]
[package.dependencies]
@@ -7220,7 +7274,7 @@ django = ["django (>=1.8)"]
falcon = ["falcon (>=1.4)"]
fastapi = ["fastapi (>=0.79.0)"]
flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"]
-grpcio = ["grpcio (>=1.21.1)"]
+grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"]
httpx = ["httpx (>=0.16.0)"]
huey = ["huey (>=2)"]
huggingface-hub = ["huggingface-hub (>=0.22)"]
@@ -7342,19 +7396,18 @@ test = ["pytest"]
[[package]]
name = "setuptools"
-version = "69.5.1"
+version = "70.0.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
- {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"},
- {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"},
+ {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"},
+ {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"},
]
[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "shapely"
@@ -7699,20 +7752,6 @@ files = [
[package.extras]
widechars = ["wcwidth"]
-[[package]]
-name = "tenacity"
-version = "8.2.3"
-description = "Retry code until it succeeds"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"},
- {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"},
-]
-
-[package.extras]
-doc = ["reno", "sphinx", "tornado (>=4.5)"]
-
[[package]]
name = "timezonefinder"
version = "6.5.0"
@@ -7767,13 +7806,13 @@ telegram = ["requests"]
[[package]]
name = "types-requests"
-version = "2.31.0.20240406"
+version = "2.32.0.20240523"
description = "Typing stubs for requests"
optional = false
python-versions = ">=3.8"
files = [
- {file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"},
- {file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"},
+ {file = "types-requests-2.32.0.20240523.tar.gz", hash = "sha256:26b8a6de32d9f561192b9942b41c0ab2d8010df5677ca8aa146289d11d505f57"},
+ {file = "types_requests-2.32.0.20240523-py3-none-any.whl", hash = "sha256:f19ed0e2daa74302069bbbbf9e82902854ffa780bc790742a810a9aaa52f65ec"},
]
[package.dependencies]
@@ -7792,13 +7831,13 @@ files = [
[[package]]
name = "typing-extensions"
-version = "4.11.0"
+version = "4.12.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
- {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
- {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
+ {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"},
+ {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"},
]
[[package]]
@@ -7831,13 +7870,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "virtualenv"
-version = "20.26.1"
+version = "20.26.2"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
- {file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"},
- {file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"},
+ {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"},
+ {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"},
]
[package.dependencies]
@@ -7986,20 +8025,20 @@ multidict = ">=4.0"
[[package]]
name = "zipp"
-version = "3.18.1"
+version = "3.18.2"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
- {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"},
- {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"},
+ {file = "zipp-3.18.2-py3-none-any.whl", hash = "sha256:dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e"},
+ {file = "zipp-3.18.2.tar.gz", hash = "sha256:6278d9ddbcfb1f1089a88fde84481528b07b0e10474e09dcfe53dad4069fa059"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
+testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[metadata]
lock-version = "2.0"
python-versions = "~3.11"
-content-hash = "5f0a1b6f26faa3effeaa5393b73d9188be385a72c1d3b9befb3f03df3b38c86d"
+content-hash = "ac6ceb0682848e6c5e39110842e8d96fdc98b15037c56491a7fc1e03b7b89cbd"
diff --git a/pyproject.toml b/pyproject.toml
index a25fcc1645..d7145b10a9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,12 @@
+[project]
+name = "openpilot"
+requires-python = ">= 3.11"
+readme = "README.md"
+license = {text = "MIT License"}
+
+[project.urls]
+Homepage = "https://comma.ai"
+
[tool.pytest.ini_options]
minversion = "6.0"
addopts = "--ignore=openpilot/ --ignore=cereal/ --ignore=opendbc/ --ignore=panda/ --ignore=rednose_repo/ --ignore=tinygrad_repo/ --ignore=teleoprtc_repo/ -Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup"
@@ -11,17 +20,17 @@ markers = [
]
testpaths = [
"common",
- "selfdrive/athena",
"selfdrive/boardd",
"selfdrive/car",
"selfdrive/controls",
"selfdrive/locationd",
"selfdrive/monitoring",
"selfdrive/navd/tests",
- "selfdrive/thermald",
"selfdrive/test/longitudinal_maneuvers",
"selfdrive/test/process_replay/test_fuzzy.py",
- "selfdrive/updated",
+ "system/updated",
+ "system/thermald",
+ "system/athena",
"system/camerad",
"system/hardware/tici",
"system/loggerd",
@@ -125,14 +134,14 @@ breathe = "*"
control = "*"
coverage = "*"
dictdiffer = "*"
-ft4222 = "*"
flaky = "*"
hypothesis = "~6.47"
inputs = "*"
Jinja2 = "*"
lru-dict = "*"
matplotlib = "*"
-metadrive-simulator = { version = "0.4.2.3", markers = "platform_machine != 'aarch64'" } # no linux/aarch64 wheels for certain dependencies
+# No release for this fix https://github.com/metadriverse/metadrive/issues/632. Pinned to this commit until next release
+metadrive-simulator = {git = "https://github.com/metadriverse/metadrive.git", rev ="233a3a1698be7038ec3dd050ca10b547b4b3324c", markers = "platform_machine != 'aarch64'" } # no linux/aarch64 wheels for certain dependencies
mpld3 = "*"
mypy = "*"
myst-parser = "*"
@@ -154,12 +163,14 @@ pytest-subtests = "*"
pytest-xdist = "*"
pytest-timeout = "*"
pytest-randomly = "*"
+pytest-asyncio = "*"
+pytest-mock = "*"
+rerun-sdk = "*"
ruff = "*"
sphinx = "*"
sphinx-rtd-theme = "*"
sphinx-sitemap = "*"
tabulate = "*"
-tenacity = "*"
types-requests = "*"
types-tabulate = "*"
tqdm = "*"
@@ -174,8 +185,24 @@ build-backend = "poetry.core.masonry.api"
# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml
[tool.ruff]
indent-width = 2
-lint.select = ["E", "F", "W", "PIE", "C4", "ISC", "RUF008", "RUF100", "A", "B", "TID251"]
-lint.ignore = ["E741", "E402", "C408", "ISC003", "B027", "B024"]
+lint.select = [
+ "E", "F", "W", "PIE", "C4", "ISC", "A", "B",
+ "NPY", # numpy
+ "UP", # pyupgrade
+ "TRY302", "TRY400", "TRY401", # try/excepts
+ "RUF008", "RUF100",
+ "TID251"
+]
+lint.ignore = [
+ "E741",
+ "E402",
+ "C408",
+ "ISC003",
+ "B027",
+ "B024",
+ "NPY002", # new numpy random syntax is worse
+ "UP038", # (x, y) -> x|y for isinstance
+]
line-length = 160
target-version="py311"
exclude = [
@@ -197,6 +224,7 @@ lint.flake8-implicit-str-concat.allow-multiline=false
"third_party".msg = "Use openpilot.third_party"
"tools".msg = "Use openpilot.tools"
"pytest.main".msg = "pytest.main requires special handling that is easy to mess up!"
+"unittest".msg = "Use pytest"
[tool.coverage.run]
concurrency = ["multiprocessing", "thread"]
diff --git a/release/build_devel.sh b/release/build_devel.sh
index 8b6816e423..7fce11ca72 100755
--- a/release/build_devel.sh
+++ b/release/build_devel.sh
@@ -1,5 +1,4 @@
#!/usr/bin/bash
-
set -ex
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
@@ -44,7 +43,7 @@ git clean -xdff
# do the files copy
echo "[-] copying files T=$SECONDS"
cd $SOURCE_DIR
-cp -pR --parents $(cat release/files_*) $TARGET_DIR/
+cp -pR --parents $(./release/release_files.py) $TARGET_DIR/
# in the directory
cd $TARGET_DIR
@@ -64,6 +63,13 @@ date: $DATETIME
master commit: $GIT_HASH
"
+# should be no submodules or LFS files
+git submodule status
+if [ ! -z "$(git lfs ls-files)" ]; then
+ echo "LFS files detected!"
+ exit 1
+fi
+
# ensure files are within GitHub's limit
BIG_FILES="$(find . -type f -not -path './.git/*' -size +95M)"
if [ ! -z "$BIG_FILES" ]; then
diff --git a/release/build_release.sh b/release/build_release.sh
index fc15cf6cdf..b68ae455c3 100755
--- a/release/build_release.sh
+++ b/release/build_release.sh
@@ -1,4 +1,6 @@
-#!/usr/bin/bash -e
+#!/usr/bin/bash
+set -e
+set -x
# git diff --name-status origin/release3-staging | grep "^A" | less
@@ -9,13 +11,6 @@ cd $DIR
BUILD_DIR=/data/openpilot
SOURCE_DIR="$(git rev-parse --show-toplevel)"
-if [ -f /TICI ]; then
- FILES_SRC="release/files_tici"
-else
- echo "no release files set"
- exit 1
-fi
-
if [ -z "$RELEASE_BRANCH" ]; then
echo "RELEASE_BRANCH is not set"
exit 1
@@ -36,8 +31,7 @@ git checkout --orphan $RELEASE_BRANCH
# do the files copy
echo "[-] copying files T=$SECONDS"
cd $SOURCE_DIR
-cp -pR --parents $(cat release/files_common) $BUILD_DIR/
-cp -pR --parents $(cat $FILES_SRC) $BUILD_DIR/
+cp -pR --parents $(./release/release_files.py) $BUILD_DIR/
# in the directory
cd $BUILD_DIR
@@ -54,7 +48,7 @@ git commit -a -m "openpilot v$VERSION release"
# Build
export PYTHONPATH="$BUILD_DIR"
-scons -j$(nproc)
+scons -j$(nproc) --minimal
# release panda fw
CERT=/data/pandaextra/certs/release RELEASE=1 scons -j$(nproc) panda/
@@ -77,6 +71,10 @@ find . -name '__pycache__' -delete
rm -rf .sconsign.dblite Jenkinsfile release/
rm selfdrive/modeld/models/supercombo.onnx
+find third_party/ -name '*x86*' -exec rm -r {} +
+find third_party/ -name '*Darwin*' -exec rm -r {} +
+
+
# Restore third_party
git checkout third_party/
@@ -93,7 +91,7 @@ cd $SOURCE_DIR
cp -pR -n --parents $TEST_FILES $BUILD_DIR/
cd $BUILD_DIR
RELEASE=1 selfdrive/test/test_onroad.py
-#selfdrive/manager/test/test_manager.py
+#system/manager/test/test_manager.py
selfdrive/car/tests/test_car_interfaces.py
rm -rf $TEST_FILES
diff --git a/release/files_common b/release/files_common
deleted file mode 100644
index 4e6bfff8e9..0000000000
--- a/release/files_common
+++ /dev/null
@@ -1,570 +0,0 @@
-.gitignore
-LICENSE
-launch_env.sh
-launch_chffrplus.sh
-launch_openpilot.sh
-
-Jenkinsfile
-SConstruct
-pyproject.toml
-
-README.md
-RELEASES.md
-docs/CARS.md
-docs/CONTRIBUTING.md
-docs/INTEGRATION.md
-docs/LIMITATIONS.md
-site_scons/site_tools/cython.py
-
-openpilot/__init__.py
-openpilot/**
-
-common/.gitignore
-common/__init__.py
-common/*.py
-common/*.pyx
-common/mock/*
-
-common/transformations/__init__.py
-common/transformations/camera.py
-common/transformations/model.py
-
-common/transformations/SConscript
-common/transformations/coordinates.py
-common/transformations/coordinates.cc
-common/transformations/coordinates.hpp
-common/transformations/orientation.py
-common/transformations/orientation.cc
-common/transformations/orientation.hpp
-common/transformations/transformations.pxd
-common/transformations/transformations.pyx
-
-common/api/__init__.py
-
-release/*
-
-tools/__init__.py
-tools/lib/*
-tools/bodyteleop/.gitignore
-tools/bodyteleop/web.py
-tools/bodyteleop/static/*
-tools/joystick/*
-tools/replay/*.cc
-tools/replay/*.h
-
-selfdrive/__init__.py
-selfdrive/sentry.py
-selfdrive/tombstoned.py
-selfdrive/statsd.py
-
-selfdrive/updated/**
-
-system/logmessaged.py
-system/micd.py
-system/version.py
-
-selfdrive/SConscript
-
-selfdrive/athena/__init__.py
-selfdrive/athena/athenad.py
-selfdrive/athena/manage_athenad.py
-selfdrive/athena/registration.py
-
-selfdrive/boardd/.gitignore
-selfdrive/boardd/SConscript
-selfdrive/boardd/__init__.py
-selfdrive/boardd/boardd.cc
-selfdrive/boardd/boardd.h
-selfdrive/boardd/main.cc
-selfdrive/boardd/boardd.py
-selfdrive/boardd/boardd_api_impl.pyx
-selfdrive/boardd/can_list_to_can_capnp.cc
-selfdrive/boardd/panda.cc
-selfdrive/boardd/panda.h
-selfdrive/boardd/spi.cc
-selfdrive/boardd/panda_comms.h
-selfdrive/boardd/panda_comms.cc
-selfdrive/boardd/pandad.py
-selfdrive/boardd/tests/test_boardd_loopback.py
-
-selfdrive/car/__init__.py
-selfdrive/car/card.py
-selfdrive/car/docs_definitions.py
-selfdrive/car/car_helpers.py
-selfdrive/car/fingerprints.py
-selfdrive/car/interfaces.py
-selfdrive/car/values.py
-selfdrive/car/vin.py
-selfdrive/car/disable_ecu.py
-selfdrive/car/fw_versions.py
-selfdrive/car/fw_query_definitions.py
-selfdrive/car/ecu_addrs.py
-selfdrive/car/isotp_parallel_query.py
-selfdrive/car/tests/__init__.py
-selfdrive/car/tests/test_car_interfaces.py
-selfdrive/car/torque_data/*
-
-selfdrive/car/body/*.py
-selfdrive/car/chrysler/*.py
-selfdrive/car/ford/*.py
-selfdrive/car/gm/*.py
-selfdrive/car/honda/*.py
-selfdrive/car/hyundai/*.py
-selfdrive/car/mazda/*.py
-selfdrive/car/mock/*.py
-selfdrive/car/nissan/*.py
-selfdrive/car/subaru/*.py
-selfdrive/car/tesla/*.py
-selfdrive/car/toyota/*.py
-selfdrive/car/volkswagen/*.py
-
-selfdrive/debug/can_printer.py
-selfdrive/debug/check_freq.py
-selfdrive/debug/dump.py
-selfdrive/debug/filter_log_message.py
-selfdrive/debug/format_fingerprints.py
-selfdrive/debug/get_fingerprint.py
-selfdrive/debug/uiview.py
-
-selfdrive/debug/hyundai_enable_radar_points.py
-selfdrive/debug/vw_mqb_config.py
-
-selfdrive/gpxd/gpx_uploader.py
-selfdrive/gpxd/gpxd.py
-
-common/SConscript
-common/version.h
-
-common/*.h
-common/*.cc
-
-selfdrive/controls/__init__.py
-selfdrive/controls/controlsd.py
-selfdrive/controls/plannerd.py
-selfdrive/controls/radard.py
-selfdrive/controls/lib/__init__.py
-selfdrive/controls/lib/alertmanager.py
-selfdrive/controls/lib/alerts_offroad.json
-selfdrive/controls/lib/desire_helper.py
-selfdrive/controls/lib/drive_helpers.py
-selfdrive/controls/lib/events.py
-selfdrive/controls/lib/latcontrol_angle.py
-selfdrive/controls/lib/latcontrol_torque.py
-selfdrive/controls/lib/latcontrol_pid.py
-selfdrive/controls/lib/latcontrol.py
-selfdrive/controls/lib/longcontrol.py
-selfdrive/controls/lib/longitudinal_planner.py
-selfdrive/controls/lib/pid.py
-selfdrive/controls/lib/vehicle_model.py
-
-selfdrive/controls/lib/lateral_mpc_lib/.gitignore
-selfdrive/controls/lib/longitudinal_mpc_lib/.gitignore
-selfdrive/controls/lib/lateral_mpc_lib/*
-selfdrive/controls/lib/longitudinal_mpc_lib/*
-
-system/__init__.py
-system/*.py
-
-system/hardware/__init__.py
-system/hardware/base.h
-system/hardware/base.py
-system/hardware/hw.h
-system/hardware/hw.py
-system/hardware/tici/__init__.py
-system/hardware/tici/hardware.h
-system/hardware/tici/hardware.py
-system/hardware/tici/pins.py
-system/hardware/tici/agnos.py
-system/hardware/tici/agnos.json
-system/hardware/tici/amplifier.py
-system/hardware/tici/updater
-system/hardware/tici/iwlist.py
-system/hardware/tici/esim.nmconnection
-system/hardware/pc/__init__.py
-system/hardware/pc/hardware.h
-system/hardware/pc/hardware.py
-
-system/ubloxd/.gitignore
-system/ubloxd/SConscript
-system/ubloxd/pigeond.py
-system/ubloxd/generated/*
-system/ubloxd/*.h
-system/ubloxd/*.cc
-
-system/updated/*
-
-selfdrive/locationd/__init__.py
-selfdrive/locationd/SConscript
-selfdrive/locationd/.gitignore
-selfdrive/locationd/locationd.h
-selfdrive/locationd/locationd.cc
-selfdrive/locationd/paramsd.py
-selfdrive/locationd/models/__init__.py
-selfdrive/locationd/models/.gitignore
-selfdrive/locationd/models/car_kf.py
-selfdrive/locationd/models/live_kf.py
-selfdrive/locationd/models/live_kf.h
-selfdrive/locationd/models/live_kf.cc
-selfdrive/locationd/models/constants.py
-
-selfdrive/locationd/torqued.py
-selfdrive/locationd/calibrationd.py
-selfdrive/locationd/helpers.py
-
-system/logcatd/.gitignore
-system/logcatd/SConscript
-system/logcatd/logcatd_systemd.cc
-
-system/proclogd/SConscript
-system/proclogd/main.cc
-system/proclogd/proclog.cc
-system/proclogd/proclog.h
-
-system/loggerd/.gitignore
-system/loggerd/SConscript
-system/loggerd/encoder/encoder.cc
-system/loggerd/encoder/encoder.h
-system/loggerd/encoder/v4l_encoder.cc
-system/loggerd/encoder/v4l_encoder.h
-system/loggerd/video_writer.cc
-system/loggerd/video_writer.h
-system/loggerd/logger.cc
-system/loggerd/logger.h
-system/loggerd/loggerd.cc
-system/loggerd/loggerd.h
-system/loggerd/encoderd.cc
-system/loggerd/bootlog.cc
-system/loggerd/encoder/ffmpeg_encoder.cc
-system/loggerd/encoder/ffmpeg_encoder.h
-
-system/loggerd/__init__.py
-system/loggerd/config.py
-system/loggerd/uploader.py
-system/loggerd/deleter.py
-system/loggerd/xattr_cache.py
-
-system/sensord/.gitignore
-system/sensord/SConscript
-system/sensord/sensors_qcom2.cc
-system/sensord/sensors/*.cc
-system/sensord/sensors/*.h
-
-system/webrtc/__init__.py
-system/webrtc/webrtcd.py
-system/webrtc/schema.py
-system/webrtc/device/audio.py
-system/webrtc/device/video.py
-
-selfdrive/thermald/thermald.py
-selfdrive/thermald/power_monitoring.py
-selfdrive/thermald/fan_controller.py
-
-selfdrive/test/__init__.py
-selfdrive/test/fuzzy_generation.py
-selfdrive/test/helpers.py
-selfdrive/test/setup_device_ci.sh
-selfdrive/test/test_onroad.py
-selfdrive/test/test_time_to_onroad.py
-
-selfdrive/ui/.gitignore
-selfdrive/ui/SConscript
-selfdrive/ui/*.cc
-selfdrive/ui/*.h
-selfdrive/ui/text
-selfdrive/ui/spinner
-selfdrive/ui/soundd.py
-selfdrive/ui/translations/*.ts
-selfdrive/ui/translations/languages.json
-selfdrive/ui/update_translations.py
-selfdrive/ui/tests/test_translations.py
-
-selfdrive/ui/qt/*.cc
-selfdrive/ui/qt/*.h
-selfdrive/ui/qt/network/*.cc
-selfdrive/ui/qt/network/*.h
-selfdrive/ui/qt/offroad/*.cc
-selfdrive/ui/qt/offroad/*.h
-selfdrive/ui/qt/offroad/*.qml
-selfdrive/ui/qt/onroad/*.cc
-selfdrive/ui/qt/onroad/*.h
-selfdrive/ui/qt/widgets/*.cc
-selfdrive/ui/qt/widgets/*.h
-selfdrive/ui/qt/maps/*.cc
-selfdrive/ui/qt/maps/*.h
-selfdrive/ui/qt/setup/*.cc
-selfdrive/ui/qt/setup/*.h
-
-selfdrive/ui/installer/*.cc
-selfdrive/ui/installer/*.h
-selfdrive/ui/installer/*.cc
-
-system/camerad/SConscript
-system/camerad/main.cc
-
-system/camerad/snapshot/*
-system/camerad/cameras/camera_common.h
-system/camerad/cameras/camera_common.cc
-system/camerad/sensors/*.h
-system/camerad/sensors/*.cc
-
-selfdrive/manager/__init__.py
-selfdrive/manager/build.py
-selfdrive/manager/helpers.py
-selfdrive/manager/manager.py
-selfdrive/manager/process_config.py
-selfdrive/manager/process.py
-selfdrive/manager/test/__init__.py
-selfdrive/manager/test/test_manager.py
-
-selfdrive/modeld/.gitignore
-selfdrive/modeld/__init__.py
-selfdrive/modeld/SConscript
-selfdrive/modeld/modeld.py
-selfdrive/modeld/parse_model_outputs.py
-selfdrive/modeld/fill_model_msg.py
-selfdrive/modeld/get_model_metadata.py
-selfdrive/modeld/dmonitoringmodeld.py
-selfdrive/modeld/constants.py
-selfdrive/modeld/modeld
-
-selfdrive/modeld/models/__init__.py
-selfdrive/modeld/models/*.pxd
-selfdrive/modeld/models/*.pyx
-
-selfdrive/modeld/models/commonmodel.cc
-selfdrive/modeld/models/commonmodel.h
-
-selfdrive/modeld/models/supercombo.onnx
-
-selfdrive/modeld/models/dmonitoring_model_q.dlc
-
-selfdrive/modeld/transforms/loadyuv.cc
-selfdrive/modeld/transforms/loadyuv.h
-selfdrive/modeld/transforms/loadyuv.cl
-selfdrive/modeld/transforms/transform.cc
-selfdrive/modeld/transforms/transform.h
-selfdrive/modeld/transforms/transform.cl
-
-selfdrive/modeld/thneed/*.py
-selfdrive/modeld/thneed/thneed.h
-selfdrive/modeld/thneed/thneed_common.cc
-selfdrive/modeld/thneed/thneed_qcom2.cc
-selfdrive/modeld/thneed/serialize.cc
-
-selfdrive/modeld/runners/__init__.py
-selfdrive/modeld/runners/*.pxd
-selfdrive/modeld/runners/*.pyx
-selfdrive/modeld/runners/*.cc
-selfdrive/modeld/runners/*.h
-selfdrive/modeld/runners/*.py
-
-selfdrive/monitoring/dmonitoringd.py
-selfdrive/monitoring/driver_monitor.py
-
-selfdrive/navd/.gitignore
-selfdrive/navd/__init__.py
-selfdrive/navd/**
-
-selfdrive/assets/.gitignore
-selfdrive/assets/assets.qrc
-selfdrive/assets/*.png
-selfdrive/assets/*.svg
-selfdrive/assets/body/*
-selfdrive/assets/fonts/*.ttf
-selfdrive/assets/icons/*
-selfdrive/assets/images/*
-selfdrive/assets/offroad/*
-selfdrive/assets/sounds/*
-selfdrive/assets/training/*
-selfdrive/assets/navigation/*
-
-third_party/.gitignore
-third_party/SConscript
-
-third_party/linux/**
-third_party/opencl/**
-
-third_party/json11/json11.cpp
-third_party/json11/json11.hpp
-
-third_party/qrcode/*.cc
-third_party/qrcode/*.hpp
-
-third_party/kaitai/*.h
-third_party/kaitai/*.cpp
-
-third_party/libyuv/include/**
-
-third_party/snpe/include/**
-third_party/snpe/dsp**
-
-third_party/acados/.gitignore
-third_party/acados/include/**
-third_party/acados/acados_template/**
-
-third_party/bootstrap/**
-third_party/qt5/larch64/bin/**
-third_party/maplibre-native-qt/**
-
-scripts/update_now.sh
-scripts/stop_updater.sh
-
-teleoprtc/**
-
-rednose_repo/site_scons/site_tools/rednose_filter.py
-rednose/.gitignore
-rednose/**
-
-body/.gitignore
-body/board/SConscript
-body/board/*.h
-body/board/*.c
-body/board/*.s
-body/board/*.ld
-body/board/inc/**
-body/board/obj/
-body/board/bldc/**
-body/board/drivers/**
-body/certs/**
-body/crypto/**
-
-cereal/.gitignore
-cereal/__init__.py
-cereal/car.capnp
-cereal/custom.capnp
-cereal/legacy.capnp
-cereal/log.capnp
-cereal/services.py
-cereal/SConscript
-cereal/include/**
-cereal/logger/logger.h
-cereal/messaging/.gitignore
-cereal/messaging/__init__.py
-cereal/messaging/bridge.cc
-cereal/messaging/event.cc
-cereal/messaging/event.h
-cereal/messaging/impl_fake.cc
-cereal/messaging/impl_fake.h
-cereal/messaging/impl_msgq.cc
-cereal/messaging/impl_msgq.h
-cereal/messaging/impl_zmq.cc
-cereal/messaging/impl_zmq.h
-cereal/messaging/messaging.cc
-cereal/messaging/messaging.h
-cereal/messaging/messaging.pxd
-cereal/messaging/messaging_pyx.pyx
-cereal/messaging/msgq.cc
-cereal/messaging/msgq.h
-cereal/messaging/socketmaster.cc
-cereal/visionipc/.gitignore
-cereal/visionipc/__init__.py
-cereal/visionipc/*.cc
-cereal/visionipc/*.h
-cereal/visionipc/*.pyx
-cereal/visionipc/*.pxd
-
-panda/.gitignore
-panda/__init__.py
-panda/SConscript
-panda/board/**
-panda/certs/**
-panda/crypto/**
-panda/examples/query_fw_versions.py
-panda/python/**
-
-opendbc/.gitignore
-opendbc/__init__.py
-opendbc/can/__init__.py
-opendbc/can/SConscript
-opendbc/can/can_define.py
-opendbc/can/common.cc
-opendbc/can/common.h
-opendbc/can/common.pxd
-opendbc/can/common_dbc.h
-opendbc/can/dbc.cc
-opendbc/can/packer.cc
-opendbc/can/packer.py
-opendbc/can/packer_pyx.pyx
-opendbc/can/parser.cc
-opendbc/can/parser.py
-opendbc/can/parser_pyx.pyx
-
-opendbc/comma_body.dbc
-
-opendbc/chrysler_ram_hd_generated.dbc
-opendbc/chrysler_ram_dt_generated.dbc
-opendbc/chrysler_pacifica_2017_hybrid_generated.dbc
-opendbc/chrysler_pacifica_2017_hybrid_private_fusion.dbc
-
-opendbc/gm_global_a_powertrain_generated.dbc
-opendbc/gm_global_a_object.dbc
-opendbc/gm_global_a_chassis.dbc
-
-opendbc/FORD_CADS.dbc
-opendbc/ford_fusion_2018_adas.dbc
-opendbc/ford_lincoln_base_pt.dbc
-
-opendbc/honda_accord_2018_can_generated.dbc
-opendbc/acura_ilx_2016_can_generated.dbc
-opendbc/acura_rdx_2018_can_generated.dbc
-opendbc/acura_rdx_2020_can_generated.dbc
-opendbc/honda_civic_touring_2016_can_generated.dbc
-opendbc/honda_civic_hatchback_ex_2017_can_generated.dbc
-opendbc/honda_crv_touring_2016_can_generated.dbc
-opendbc/honda_crv_ex_2017_can_generated.dbc
-opendbc/honda_crv_ex_2017_body_generated.dbc
-opendbc/honda_crv_executive_2016_can_generated.dbc
-opendbc/honda_fit_ex_2018_can_generated.dbc
-opendbc/honda_odyssey_exl_2018_generated.dbc
-opendbc/honda_odyssey_extreme_edition_2018_china_can_generated.dbc
-opendbc/honda_insight_ex_2019_can_generated.dbc
-opendbc/acura_ilx_2016_nidec.dbc
-opendbc/honda_civic_ex_2022_can_generated.dbc
-
-opendbc/hyundai_canfd.dbc
-opendbc/hyundai_kia_generic.dbc
-opendbc/hyundai_kia_mando_front_radar_generated.dbc
-
-opendbc/mazda_2017.dbc
-
-opendbc/nissan_x_trail_2017_generated.dbc
-opendbc/nissan_leaf_2018_generated.dbc
-
-opendbc/subaru_global_2017_generated.dbc
-opendbc/subaru_global_2020_hybrid_generated.dbc
-opendbc/subaru_outback_2015_generated.dbc
-opendbc/subaru_outback_2019_generated.dbc
-opendbc/subaru_forester_2017_generated.dbc
-
-opendbc/toyota_tnga_k_pt_generated.dbc
-opendbc/toyota_new_mc_pt_generated.dbc
-opendbc/toyota_nodsu_pt_generated.dbc
-opendbc/toyota_adas.dbc
-opendbc/toyota_tss2_adas.dbc
-
-opendbc/vw_golf_mk4.dbc
-opendbc/vw_mqb_2010.dbc
-
-opendbc/tesla_can.dbc
-opendbc/tesla_radar_bosch_generated.dbc
-opendbc/tesla_radar_continental_generated.dbc
-opendbc/tesla_powertrain.dbc
-
-tinygrad_repo/openpilot/compile2.py
-tinygrad_repo/extra/onnx.py
-tinygrad_repo/extra/onnx_ops.py
-tinygrad_repo/extra/thneed.py
-tinygrad_repo/extra/utils.py
-tinygrad_repo/tinygrad/codegen/kernel.py
-tinygrad_repo/tinygrad/codegen/linearizer.py
-tinygrad_repo/tinygrad/features/image.py
-tinygrad_repo/tinygrad/features/search.py
-tinygrad_repo/tinygrad/nn/*
-tinygrad_repo/tinygrad/renderer/cstyle.py
-tinygrad_repo/tinygrad/renderer/opencl.py
-tinygrad_repo/tinygrad/runtime/lib.py
-tinygrad_repo/tinygrad/runtime/ops_cpu.py
-tinygrad_repo/tinygrad/runtime/ops_disk.py
-tinygrad_repo/tinygrad/runtime/ops_gpu.py
-tinygrad_repo/tinygrad/shape/*
-tinygrad_repo/tinygrad/*.py
diff --git a/release/files_pc b/release/files_pc
deleted file mode 100644
index f2bf090f2c..0000000000
--- a/release/files_pc
+++ /dev/null
@@ -1,4 +0,0 @@
-third_party/libyuv/x86_64/**
-third_party/snpe/x86_64/**
-third_party/snpe/x86_64-linux-clang/**
-third_party/acados/x86_64/**
diff --git a/release/files_tici b/release/files_tici
deleted file mode 100644
index 18860e20af..0000000000
--- a/release/files_tici
+++ /dev/null
@@ -1,15 +0,0 @@
-third_party/libyuv/larch64/**
-third_party/snpe/larch64**
-third_party/snpe/aarch64-ubuntu-gcc7.5/*
-third_party/acados/larch64/**
-
-system/camerad/cameras/camera_qcom2.cc
-system/camerad/cameras/camera_qcom2.h
-system/camerad/cameras/camera_util.cc
-system/camerad/cameras/camera_util.h
-system/camerad/cameras/process_raw.cl
-
-system/qcomgpsd/*
-
-selfdrive/ui/qt/spinner_larch64
-selfdrive/ui/qt/text_larch64
diff --git a/release/release_files.py b/release/release_files.py
new file mode 100755
index 0000000000..a6fb7d6481
--- /dev/null
+++ b/release/release_files.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+import os
+import re
+from pathlib import Path
+
+HERE = os.path.abspath(os.path.dirname(__file__))
+ROOT = HERE + "/.."
+
+# blacklisting is for two purposes:
+# - minimizing release download size
+# - keeping the diff readable
+blacklist = [
+ "^scripts/",
+ "body/STL/",
+ "tools/cabana/",
+ "panda/examples/",
+ "opendbc/generator/",
+
+ "^tools/",
+ "^tinygrad_repo/",
+
+ "matlab.*.md",
+
+ ".git$", # for submodules
+ ".git/",
+ ".github/",
+ ".devcontainer/",
+ "Darwin/",
+ ".vscode",
+
+ # no LFS
+ ".lfsconfig",
+ ".gitattributes",
+]
+
+# gets you through the blacklist
+whitelist = [
+ "tools/lib/",
+ "tools/bodyteleop/",
+
+ "tinygrad_repo/openpilot/compile2.py",
+ "tinygrad_repo/extra/onnx.py",
+ "tinygrad_repo/extra/onnx_ops.py",
+ "tinygrad_repo/extra/thneed.py",
+ "tinygrad_repo/extra/utils.py",
+ "tinygrad_repo/tinygrad/codegen/kernel.py",
+ "tinygrad_repo/tinygrad/codegen/linearizer.py",
+ "tinygrad_repo/tinygrad/features/image.py",
+ "tinygrad_repo/tinygrad/features/search.py",
+ "tinygrad_repo/tinygrad/nn/*",
+ "tinygrad_repo/tinygrad/renderer/cstyle.py",
+ "tinygrad_repo/tinygrad/renderer/opencl.py",
+ "tinygrad_repo/tinygrad/runtime/lib.py",
+ "tinygrad_repo/tinygrad/runtime/ops_cpu.py",
+ "tinygrad_repo/tinygrad/runtime/ops_disk.py",
+ "tinygrad_repo/tinygrad/runtime/ops_gpu.py",
+ "tinygrad_repo/tinygrad/shape/*",
+ "tinygrad_repo/tinygrad/.*.py",
+]
+
+if __name__ == "__main__":
+ for f in Path(ROOT).rglob("**/*"):
+ if not (f.is_file() or f.is_symlink()):
+ continue
+
+ rf = str(f.relative_to(ROOT))
+ blacklisted = any(re.search(p, rf) for p in blacklist)
+ whitelisted = any(re.search(p, rf) for p in whitelist)
+ if blacklisted and not whitelisted:
+ continue
+
+ print(rf)
diff --git a/scripts/stop_updater.sh b/scripts/stop_updater.sh
index 7f82191823..703b363928 100755
--- a/scripts/stop_updater.sh
+++ b/scripts/stop_updater.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env sh
# Stop updater
-pkill -2 -f selfdrive.updated.updated
+pkill -2 -f system.updated.updated
# Remove pending update
rm -f /data/safe_staging/finalized/.overlay_consistent
diff --git a/scripts/update_now.sh b/scripts/update_now.sh
index 3f0193f081..c34228976a 100755
--- a/scripts/update_now.sh
+++ b/scripts/update_now.sh
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
# Send SIGHUP to updater
-pkill -1 -f selfdrive.updated
+pkill -1 -f system.updated
diff --git a/selfdrive/athena/tests/test_registration.py b/selfdrive/athena/tests/test_registration.py
deleted file mode 100755
index e7ad63a370..0000000000
--- a/selfdrive/athena/tests/test_registration.py
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/env python3
-import json
-import unittest
-from Crypto.PublicKey import RSA
-from pathlib import Path
-from unittest import mock
-
-from openpilot.common.params import Params
-from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID
-from openpilot.selfdrive.athena.tests.helpers import MockResponse
-from openpilot.system.hardware.hw import Paths
-
-
-class TestRegistration(unittest.TestCase):
-
- def setUp(self):
- # clear params and setup key paths
- self.params = Params()
- self.params.clear_all()
-
- persist_dir = Path(Paths.persist_root()) / "comma"
- persist_dir.mkdir(parents=True, exist_ok=True)
-
- self.priv_key = persist_dir / "id_rsa"
- self.pub_key = persist_dir / "id_rsa.pub"
-
- def _generate_keys(self):
- self.pub_key.touch()
- k = RSA.generate(2048)
- with open(self.priv_key, "wb") as f:
- f.write(k.export_key())
- with open(self.pub_key, "wb") as f:
- f.write(k.publickey().export_key())
-
- def test_valid_cache(self):
- # if all params are written, return the cached dongle id
- self.params.put("IMEI", "imei")
- self.params.put("HardwareSerial", "serial")
- self._generate_keys()
-
- with mock.patch("openpilot.selfdrive.athena.registration.api_get", autospec=True) as m:
- dongle = "DONGLE_ID_123"
- self.params.put("DongleId", dongle)
- self.assertEqual(register(), dongle)
- self.assertFalse(m.called)
-
- def test_no_keys(self):
- # missing pubkey
- with mock.patch("openpilot.selfdrive.athena.registration.api_get", autospec=True) as m:
- dongle = register()
- self.assertEqual(m.call_count, 0)
- self.assertEqual(dongle, UNREGISTERED_DONGLE_ID)
- self.assertEqual(self.params.get("DongleId", encoding='utf-8'), dongle)
-
- def test_missing_cache(self):
- # keys exist but no dongle id
- self._generate_keys()
- with mock.patch("openpilot.selfdrive.athena.registration.api_get", autospec=True) as m:
- dongle = "DONGLE_ID_123"
- m.return_value = MockResponse(json.dumps({'dongle_id': dongle}), 200)
- self.assertEqual(register(), dongle)
- self.assertEqual(m.call_count, 1)
-
- # call again, shouldn't hit the API this time
- self.assertEqual(register(), dongle)
- self.assertEqual(m.call_count, 1)
- self.assertEqual(self.params.get("DongleId", encoding='utf-8'), dongle)
-
- def test_unregistered(self):
- # keys exist, but unregistered
- self._generate_keys()
- with mock.patch("openpilot.selfdrive.athena.registration.api_get", autospec=True) as m:
- m.return_value = MockResponse(None, 402)
- dongle = register()
- self.assertEqual(m.call_count, 1)
- self.assertEqual(dongle, UNREGISTERED_DONGLE_ID)
- self.assertEqual(self.params.get("DongleId", encoding='utf-8'), dongle)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/boardd/boardd_api_impl.pyx b/selfdrive/boardd/boardd_api_impl.pyx
index 6a552bb447..cbac8cc5a3 100644
--- a/selfdrive/boardd/boardd_api_impl.pyx
+++ b/selfdrive/boardd/boardd_api_impl.pyx
@@ -15,16 +15,17 @@ cdef extern from "can_list_to_can_capnp.cc":
void can_list_to_can_capnp_cpp(const vector[can_frame] &can_list, string &out, bool sendCan, bool valid)
def can_list_to_can_capnp(can_msgs, msgtype='can', valid=True):
+ cdef can_frame *f
cdef vector[can_frame] can_list
- can_list.reserve(len(can_msgs))
- cdef can_frame f
+ can_list.reserve(len(can_msgs))
for can_msg in can_msgs:
+ f = &(can_list.emplace_back())
f.address = can_msg[0]
f.busTime = can_msg[1]
f.dat = can_msg[2]
f.src = can_msg[3]
- can_list.push_back(f)
+
cdef string out
can_list_to_can_capnp_cpp(can_list, out, msgtype == 'sendcan', valid)
return out
diff --git a/selfdrive/boardd/spi.cc b/selfdrive/boardd/spi.cc
index e02252018f..66e6a0f0ab 100644
--- a/selfdrive/boardd/spi.cc
+++ b/selfdrive/boardd/spi.cc
@@ -301,7 +301,11 @@ int PandaSpiHandle::lltransfer(spi_ioc_transfer &t) {
}
if ((static_cast(rand()) / RAND_MAX) < err_prob && t.tx_buf != (uint64_t)NULL) {
printf("corrupting TX\n");
- memset((uint8_t*)t.tx_buf, (uint8_t)(rand() % 256), rand() % (t.len+1));
+ for (int i = 0; i < t.len; i++) {
+ if ((static_cast(rand()) / RAND_MAX) > 0.9) {
+ ((uint8_t*)t.tx_buf)[i] = (uint8_t)(rand() % 256);
+ }
+ }
}
}
@@ -310,7 +314,11 @@ int PandaSpiHandle::lltransfer(spi_ioc_transfer &t) {
if (err_prob > 0) {
if ((static_cast(rand()) / RAND_MAX) < err_prob && t.rx_buf != (uint64_t)NULL) {
printf("corrupting RX\n");
- memset((uint8_t*)t.rx_buf, (uint8_t)(rand() % 256), rand() % (t.len+1));
+ for (int i = 0; i < t.len; i++) {
+ if ((static_cast(rand()) / RAND_MAX) > 0.9) {
+ ((uint8_t*)t.rx_buf)[i] = (uint8_t)(rand() % 256);
+ }
+ }
}
}
@@ -345,13 +353,13 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
ret = lltransfer(transfer);
if (ret < 0) {
LOGE("SPI: failed to send header");
- goto transfer_fail;
+ return ret;
}
// Wait for (N)ACK
ret = wait_for_ack(SPI_HACK, 0x11, timeout, 1);
if (ret < 0) {
- goto transfer_fail;
+ return ret;
}
// Send data
@@ -363,20 +371,20 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
ret = lltransfer(transfer);
if (ret < 0) {
LOGE("SPI: failed to send data");
- goto transfer_fail;
+ return ret;
}
// Wait for (N)ACK
ret = wait_for_ack(SPI_DACK, 0x13, timeout, 3);
if (ret < 0) {
- goto transfer_fail;
+ return ret;
}
// Read data
rx_data_len = *(uint16_t *)(rx_buf+1);
if (rx_data_len >= SPI_BUF_SIZE) {
LOGE("SPI: RX data len larger than buf size %d", rx_data_len);
- goto transfer_fail;
+ return -1;
}
transfer.len = rx_data_len + 1;
@@ -384,11 +392,11 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
ret = lltransfer(transfer);
if (ret < 0) {
LOGE("SPI: failed to read rx data");
- goto transfer_fail;
+ return ret;
}
if (!check_checksum(rx_buf, rx_data_len + 4)) {
LOGE("SPI: bad checksum");
- goto transfer_fail;
+ return -1;
}
if (rx_data != NULL) {
@@ -396,8 +404,5 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
}
return rx_data_len;
-
-transfer_fail:
- return ret;
}
#endif
diff --git a/selfdrive/boardd/tests/test_boardd_loopback.py b/selfdrive/boardd/tests/test_boardd_loopback.py
index 148ce9a25d..fa9eb957c2 100755
--- a/selfdrive/boardd/tests/test_boardd_loopback.py
+++ b/selfdrive/boardd/tests/test_boardd_loopback.py
@@ -3,7 +3,7 @@ import os
import copy
import random
import time
-import unittest
+import pytest
from collections import defaultdict
from pprint import pprint
@@ -17,42 +17,61 @@ from openpilot.system.hardware import TICI
from openpilot.selfdrive.test.helpers import phone_only, with_processes
-class TestBoardd(unittest.TestCase):
+def setup_boardd(num_pandas):
+ params = Params()
+ params.put_bool("IsOnroad", False)
+ with Timeout(90, "boardd didn't start"):
+ sm = messaging.SubMaster(['pandaStates'])
+ while sm.recv_frame['pandaStates'] < 1 or len(sm['pandaStates']) == 0 or \
+ any(ps.pandaType == log.PandaState.PandaType.unknown for ps in sm['pandaStates']):
+ sm.update(1000)
+
+ found_pandas = len(sm['pandaStates'])
+ assert num_pandas == found_pandas, "connected pandas ({found_pandas}) doesn't match expected panda count ({num_pandas}). \
+ connect another panda for multipanda tests."
+
+ # boardd safety setting relies on these params
+ cp = car.CarParams.new_message()
+
+ safety_config = car.CarParams.SafetyConfig.new_message()
+ safety_config.safetyModel = car.CarParams.SafetyModel.allOutput
+ cp.safetyConfigs = [safety_config]*num_pandas
+
+ params.put_bool("IsOnroad", True)
+ params.put_bool("FirmwareQueryDone", True)
+ params.put_bool("ControlsReady", True)
+ params.put("CarParams", cp.to_bytes())
+
+
+def send_random_can_messages(sendcan, count, num_pandas=1):
+ sent_msgs = defaultdict(set)
+ for _ in range(count):
+ to_send = []
+ for __ in range(random.randrange(20)):
+ bus = random.choice([b for b in range(3*num_pandas) if b % 4 != 3])
+ addr = random.randrange(1, 1<<29)
+ dat = bytes(random.getrandbits(8) for _ in range(random.randrange(1, 9)))
+ if (addr, dat) in sent_msgs[bus]:
+ continue
+ sent_msgs[bus].add((addr, dat))
+ to_send.append(make_can_msg(addr, dat, bus))
+ sendcan.send(can_list_to_can_capnp(to_send, msgtype='sendcan'))
+ return sent_msgs
+
+
+@pytest.mark.tici
+class TestBoarddLoopback:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
os.environ['STARTED'] = '1'
os.environ['BOARDD_LOOPBACK'] = '1'
@phone_only
@with_processes(['pandad'])
def test_loopback(self):
- params = Params()
- params.put_bool("IsOnroad", False)
-
- with Timeout(90, "boardd didn't start"):
- sm = messaging.SubMaster(['pandaStates'])
- while sm.recv_frame['pandaStates'] < 1 or len(sm['pandaStates']) == 0 or \
- any(ps.pandaType == log.PandaState.PandaType.unknown for ps in sm['pandaStates']):
- sm.update(1000)
-
- num_pandas = len(sm['pandaStates'])
- expected_pandas = 2 if TICI and "SINGLE_PANDA" not in os.environ else 1
- self.assertEqual(num_pandas, expected_pandas, "connected pandas ({num_pandas}) doesn't match expected panda count ({expected_pandas}). \
- connect another panda for multipanda tests.")
-
- # boardd safety setting relies on these params
- cp = car.CarParams.new_message()
-
- safety_config = car.CarParams.SafetyConfig.new_message()
- safety_config.safetyModel = car.CarParams.SafetyModel.allOutput
- cp.safetyConfigs = [safety_config]*num_pandas
-
- params.put_bool("IsOnroad", True)
- params.put_bool("FirmwareQueryDone", True)
- params.put_bool("ControlsReady", True)
- params.put("CarParams", cp.to_bytes())
-
+ num_pandas = 2 if TICI and "SINGLE_PANDA" not in os.environ else 1
+ setup_boardd(num_pandas)
sendcan = messaging.pub_sock('sendcan')
can = messaging.sub_sock('can', conflate=False, timeout=100)
sm = messaging.SubMaster(['pandaStates'])
@@ -62,16 +81,7 @@ class TestBoardd(unittest.TestCase):
for i in range(n):
print(f"boardd loopback {i}/{n}")
- sent_msgs = defaultdict(set)
- for _ in range(random.randrange(20, 100)):
- to_send = []
- for __ in range(random.randrange(20)):
- bus = random.choice([b for b in range(3*num_pandas) if b % 4 != 3])
- addr = random.randrange(1, 1<<29)
- dat = bytes(random.getrandbits(8) for _ in range(random.randrange(1, 9)))
- sent_msgs[bus].add((addr, dat))
- to_send.append(make_can_msg(addr, dat, bus))
- sendcan.send(can_list_to_can_capnp(to_send, msgtype='sendcan'))
+ sent_msgs = send_random_can_messages(sendcan, random.randrange(20, 100), num_pandas)
sent_loopback = copy.deepcopy(sent_msgs)
sent_loopback.update({k+128: copy.deepcopy(v) for k, v in sent_msgs.items()})
@@ -96,7 +106,3 @@ class TestBoardd(unittest.TestCase):
pprint(sm['pandaStates']) # may drop messages due to RX buffer overflow
for bus in sent_loopback.keys():
assert not len(sent_loopback[bus]), f"loop {i}: bus {bus} missing {len(sent_loopback[bus])} out of {sent_total[bus]} messages"
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/boardd/tests/test_boardd_spi.py b/selfdrive/boardd/tests/test_boardd_spi.py
new file mode 100755
index 0000000000..d384caaddd
--- /dev/null
+++ b/selfdrive/boardd/tests/test_boardd_spi.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+import os
+import time
+import numpy as np
+import pytest
+
+import cereal.messaging as messaging
+from cereal.services import SERVICE_LIST
+from openpilot.system.hardware import HARDWARE
+from openpilot.selfdrive.test.helpers import phone_only, with_processes
+from openpilot.selfdrive.boardd.tests.test_boardd_loopback import setup_boardd
+
+
+@pytest.mark.tici
+class TestBoarddSpi:
+ @classmethod
+ def setup_class(cls):
+ if HARDWARE.get_device_type() == 'tici':
+ pytest.skip("only for spi pandas")
+ os.environ['STARTED'] = '1'
+ os.environ['BOARDD_LOOPBACK'] = '1'
+ os.environ['SPI_ERR_PROB'] = '0.001'
+
+ @phone_only
+ @with_processes(['pandad'])
+ def test_spi_corruption(self, subtests):
+ setup_boardd(1)
+
+ socks = {s: messaging.sub_sock(s, conflate=False, timeout=100) for s in ('can', 'pandaStates', 'peripheralState')}
+ time.sleep(2)
+ for s in socks.values():
+ messaging.drain_sock_raw(s)
+
+ st = time.monotonic()
+ ts = {s: list() for s in socks.keys()}
+ for _ in range(20):
+ for service, sock in socks.items():
+ for m in messaging.drain_sock(sock):
+ ts[service].append(m.logMonoTime)
+
+ # sanity check for corruption
+ assert m.valid
+ if service == "can":
+ assert len(m.can) == 0
+ elif service == "pandaStates":
+ assert len(m.pandaStates) == 1
+ ps = m.pandaStates[0]
+ assert ps.uptime < 100
+ assert ps.pandaType == "tres"
+ assert ps.ignitionLine
+ assert not ps.ignitionCan
+ assert ps.voltage < 14000
+ elif service == "peripheralState":
+ ps = m.peripheralState
+ assert ps.pandaType == "tres"
+ assert 4000 < ps.voltage < 14000
+ assert 100 < ps.current < 1000
+ assert ps.fanSpeedRpm < 8000
+
+ time.sleep(0.5)
+ et = time.monotonic() - st
+
+ print("\n======== timing report ========")
+ for service, times in ts.items():
+ dts = np.diff(times)/1e6
+ print(service.ljust(17), f"{np.mean(dts):7.2f} {np.min(dts):7.2f} {np.max(dts):7.2f}")
+ with subtests.test(msg="timing check", service=service):
+ edt = 1e3 / SERVICE_LIST[service].frequency
+ assert edt*0.9 < np.mean(dts) < edt*1.1
+ assert np.max(dts) < edt*3
+ assert np.min(dts) < edt
+ assert len(dts) >= ((et-0.5)*SERVICE_LIST[service].frequency*0.8)
diff --git a/selfdrive/boardd/tests/test_pandad.py b/selfdrive/boardd/tests/test_pandad.py
index 3434be3fe4..581b9813a4 100755
--- a/selfdrive/boardd/tests/test_pandad.py
+++ b/selfdrive/boardd/tests/test_pandad.py
@@ -2,13 +2,12 @@
import os
import pytest
import time
-import unittest
import cereal.messaging as messaging
from cereal import log
from openpilot.common.gpio import gpio_set, gpio_init
from panda import Panda, PandaDFU, PandaProtocolMismatch
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
from openpilot.system.hardware import HARDWARE
from openpilot.system.hardware.tici.pins import GPIO
@@ -16,16 +15,16 @@ HERE = os.path.dirname(os.path.realpath(__file__))
@pytest.mark.tici
-class TestPandad(unittest.TestCase):
+class TestPandad:
- def setUp(self):
+ def setup_method(self):
# ensure panda is up
if len(Panda.list()) == 0:
self._run_test(60)
self.spi = HARDWARE.get_device_type() != 'tici'
- def tearDown(self):
+ def teardown_method(self):
managed_processes['pandad'].stop()
def _run_test(self, timeout=30) -> float:
@@ -65,7 +64,7 @@ class TestPandad(unittest.TestCase):
assert Panda.wait_for_panda(None, 10)
if expect_mismatch:
- with self.assertRaises(PandaProtocolMismatch):
+ with pytest.raises(PandaProtocolMismatch):
Panda()
else:
with Panda() as p:
@@ -108,9 +107,10 @@ class TestPandad(unittest.TestCase):
assert 0.1 < (sum(ts)/len(ts)) < (0.5 if self.spi else 5.0)
print("startup times", ts, sum(ts) / len(ts))
+
def test_protocol_version_check(self):
if not self.spi:
- raise unittest.SkipTest("SPI test")
+ pytest.skip("SPI test")
# flash old fw
fn = os.path.join(HERE, "bootstub.panda_h7_spiv0.bin")
self._flash_bootstub_and_test(fn, expect_mismatch=True)
@@ -127,7 +127,3 @@ class TestPandad(unittest.TestCase):
self._assert_no_panda()
self._run_test(60)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/car/body/carcontroller.py b/selfdrive/car/body/carcontroller.py
index db34320caa..259126c416 100644
--- a/selfdrive/car/body/carcontroller.py
+++ b/selfdrive/car/body/carcontroller.py
@@ -75,7 +75,7 @@ class CarController(CarControllerBase):
can_sends = []
can_sends.append(bodycan.create_control(self.packer, torque_l, torque_r))
- new_actuators = CC.actuators.copy()
+ new_actuators = CC.actuators.as_builder()
new_actuators.accel = torque_l
new_actuators.steer = torque_r
new_actuators.steerOutputCan = torque_r
diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py
index 8d1ef70d62..199c425782 100755
--- a/selfdrive/car/card.py
+++ b/selfdrive/car/card.py
@@ -9,30 +9,34 @@ from cereal import car
from panda import ALTERNATIVE_EXPERIENCE
from openpilot.common.params import Params
-from openpilot.common.realtime import DT_CTRL
+from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper, DT_CTRL
from openpilot.selfdrive.boardd.boardd import can_list_to_can_capnp
from openpilot.selfdrive.car.car_helpers import get_car, get_one_can
from openpilot.selfdrive.car.interfaces import CarInterfaceBase
+from openpilot.selfdrive.controls.lib.events import Events
REPLAY = "REPLAY" in os.environ
+EventName = car.CarEvent.EventName
-class CarD:
+
+class Car:
CI: CarInterfaceBase
- CS: car.CarState
def __init__(self, CI=None):
self.can_sock = messaging.sub_sock('can', timeout=20)
- self.sm = messaging.SubMaster(['pandaStates'])
+ self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents'])
self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput'])
self.can_rcv_timeout_counter = 0 # consecutive timeout count
self.can_rcv_cum_timeout_counter = 0 # cumulative timeout count
self.CC_prev = car.CarControl.new_message()
+ self.CS_prev = car.CarState.new_message()
+ self.initialized_prev = False
- self.last_actuators = None
+ self.last_actuators_output = car.CarControl.Actuators.new_message()
self.params = Params()
@@ -48,9 +52,9 @@ class CarD:
self.CI, self.CP = CI, CI.CP
# set alternative experiences from parameters
- disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
+ self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
self.CP.alternativeExperience = 0
- if not disengage_on_accelerator:
+ if not self.disengage_on_accelerator:
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS
openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle")
@@ -74,16 +78,17 @@ class CarD:
self.params.put_nonblocking("CarParamsCache", cp_bytes)
self.params.put_nonblocking("CarParamsPersistent", cp_bytes)
- def initialize(self):
- """Initialize CarInterface, once controls are ready"""
- self.CI.init(self.CP, self.can_sock, self.pm.sock['sendcan'])
+ self.events = Events()
- def state_update(self):
+ # card is driven by can recv, expected at 100Hz
+ self.rk = Ratekeeper(100, print_delay_threshold=None)
+
+ def state_update(self) -> car.CarState:
"""carState update loop, driven by can"""
# Update carState from CAN
can_strs = messaging.drain_sock_raw(self.can_sock, wait_for_one=True)
- self.CS = self.CI.update(self.CC_prev, can_strs)
+ CS = self.CI.update(self.CC_prev, can_strs)
self.sm.update(0)
@@ -101,21 +106,26 @@ class CarD:
if can_rcv_valid and REPLAY:
self.can_log_mono_time = messaging.log_from_bytes(can_strs[0]).logMonoTime
- self.state_publish()
+ return CS
- return self.CS
+ def update_events(self, CS: car.CarState) -> car.CarState:
+ self.events.clear()
- def state_publish(self):
+ self.events.add_from_msg(CS.events)
+
+ # Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0
+ if (CS.gasPressed and not self.CS_prev.gasPressed and self.disengage_on_accelerator) or \
+ (CS.brakePressed and (not self.CS_prev.brakePressed or not CS.standstill)) or \
+ (CS.regenBraking and (not self.CS_prev.regenBraking or not CS.standstill)):
+ self.events.add(EventName.pedalPressed)
+
+ CS.events = self.events.to_msg()
+
+ def state_publish(self, CS: car.CarState):
"""carState and carParams publish loop"""
- # carState
- cs_send = messaging.new_message('carState')
- cs_send.valid = self.CS.canValid
- cs_send.carState = self.CS
- self.pm.send('carState', cs_send)
-
# carParams - logged every 50 seconds (> 1 per segment)
- if (self.sm.frame % int(50. / DT_CTRL) == 0):
+ if self.sm.frame % int(50. / DT_CTRL) == 0:
cp_send = messaging.new_message('carParams')
cp_send.valid = True
cp_send.carParams = self.CP
@@ -123,17 +133,63 @@ class CarD:
# publish new carOutput
co_send = messaging.new_message('carOutput')
- co_send.valid = True
- if self.last_actuators is not None:
- co_send.carOutput.actuatorsOutput = self.last_actuators
+ co_send.valid = self.sm.all_checks(['carControl'])
+ co_send.carOutput.actuatorsOutput = self.last_actuators_output
self.pm.send('carOutput', co_send)
- def controls_update(self, CC: car.CarControl):
+ # kick off controlsd step while we actuate the latest carControl packet
+ cs_send = messaging.new_message('carState')
+ cs_send.valid = CS.canValid
+ cs_send.carState = CS
+ cs_send.carState.canRcvTimeout = self.can_rcv_timeout
+ cs_send.carState.canErrorCounter = self.can_rcv_cum_timeout_counter
+ cs_send.carState.cumLagMs = -self.rk.remaining * 1000.
+ self.pm.send('carState', cs_send)
+
+ def controls_update(self, CS: car.CarState, CC: car.CarControl):
"""control update loop, driven by carControl"""
- # send car controls over can
- now_nanos = self.can_log_mono_time if REPLAY else int(time.monotonic() * 1e9)
- self.last_actuators, can_sends = self.CI.apply(CC, now_nanos)
- self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=self.CS.canValid))
+ if not self.initialized_prev:
+ # Initialize CarInterface, once controls are ready
+ # TODO: this can make us miss at least a few cycles when doing an ECU knockout
+ self.CI.init(self.CP, self.can_sock, self.pm.sock['sendcan'])
+ # signal boardd to switch to car safety mode
+ self.params.put_bool_nonblocking("ControlsReady", True)
- self.CC_prev = CC
+ if self.sm.all_alive(['carControl']):
+ # send car controls over can
+ now_nanos = self.can_log_mono_time if REPLAY else int(time.monotonic() * 1e9)
+ self.last_actuators_output, can_sends = self.CI.apply(CC, now_nanos)
+ self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=CS.canValid))
+
+ self.CC_prev = CC
+
+ def step(self):
+ CS = self.state_update()
+
+ self.update_events(CS)
+
+ self.state_publish(CS)
+
+ initialized = (not any(e.name == EventName.controlsInitializing for e in self.sm['onroadEvents']) and
+ self.sm.seen['onroadEvents'])
+ if not self.CP.passive and initialized:
+ self.controls_update(CS, self.sm['carControl'])
+
+ self.initialized_prev = initialized
+ self.CS_prev = CS.as_reader()
+
+ def card_thread(self):
+ while True:
+ self.step()
+ self.rk.monitor_time()
+
+
+def main():
+ config_realtime_process(4, Priority.CTRL_HIGH)
+ car = Car()
+ car.card_thread()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py
index 082d1293dd..efeb4b3997 100644
--- a/selfdrive/car/chrysler/carcontroller.py
+++ b/selfdrive/car/chrysler/carcontroller.py
@@ -83,7 +83,7 @@ class CarController(CarControllerBase):
self.frame += 1
- new_actuators = CC.actuators.copy()
+ new_actuators = CC.actuators.as_builder()
new_actuators.steer = self.apply_steer_last / self.params.STEER_MAX
new_actuators.steerOutputCan = self.apply_steer_last
diff --git a/selfdrive/car/chrysler/fingerprints.py b/selfdrive/car/chrysler/fingerprints.py
index 95f878a5b6..1789054580 100644
--- a/selfdrive/car/chrysler/fingerprints.py
+++ b/selfdrive/car/chrysler/fingerprints.py
@@ -68,6 +68,7 @@ FW_VERSIONS = {
(Ecu.engine, 0x7e0, None): [
b'68267018AO ',
b'68267020AJ ',
+ b'68303534AG ',
b'68303534AJ ',
b'68340762AD ',
b'68340764AD ',
@@ -106,6 +107,7 @@ FW_VERSIONS = {
b'68405565AB',
b'68405565AC',
b'68444299AC',
+ b'68480707AC',
b'68480708AC',
b'68526663AB',
],
@@ -143,13 +145,16 @@ FW_VERSIONS = {
b'68413871AE ',
b'68413871AH ',
b'68413871AI ',
+ b'68413873AH ',
b'68413873AI ',
b'68443120AE ',
b'68443123AC ',
b'68443125AC ',
+ b'68496647AI ',
b'68496647AJ ',
b'68496650AH ',
b'68496650AI ',
+ b'68496652AH ',
b'68526752AD ',
b'68526752AE ',
b'68526754AE ',
@@ -161,10 +166,12 @@ FW_VERSIONS = {
b'68414271AC',
b'68414271AD',
b'68414275AC',
+ b'68414275AD',
b'68443154AB',
b'68443155AC',
b'68443158AB',
b'68501050AD',
+ b'68501051AD',
b'68501055AD',
b'68527221AB',
b'68527223AB',
@@ -264,8 +271,10 @@ FW_VERSIONS = {
},
CAR.JEEP_GRAND_CHEROKEE: {
(Ecu.combinationMeter, 0x742, None): [
+ b'68243549AG',
b'68302211AC',
b'68302212AD',
+ b'68302223AC',
b'68302246AC',
b'68331511AC',
b'68331574AC',
@@ -274,18 +283,22 @@ FW_VERSIONS = {
b'68340272AD',
],
(Ecu.srs, 0x744, None): [
+ b'68309533AA',
b'68316742AB',
b'68355363AB',
],
(Ecu.abs, 0x747, None): [
+ b'68252642AG',
b'68306178AD',
b'68336276AB',
],
(Ecu.fwdRadar, 0x753, None): [
b'04672627AB',
+ b'68251506AF',
b'68332015AB',
],
(Ecu.eps, 0x75a, None): [
+ b'68276201AG',
b'68321644AB',
b'68321644AC',
b'68321646AC',
@@ -293,6 +306,7 @@ FW_VERSIONS = {
],
(Ecu.engine, 0x7e0, None): [
b'05035920AE ',
+ b'68252272AG ',
b'68284455AI ',
b'68284456AI ',
b'68284477AF ',
@@ -303,6 +317,7 @@ FW_VERSIONS = {
],
(Ecu.transmission, 0x7e1, None): [
b'05035517AH',
+ b'68253222AF',
b'68311218AC',
b'68311223AF',
b'68311223AG',
@@ -422,7 +437,9 @@ FW_VERSIONS = {
b'68527403AD',
b'68546047AF',
b'68631938AA',
+ b'68631939AA',
b'68631940AA',
+ b'68631940AB',
b'68631942AA',
b'68631943AB',
],
@@ -478,6 +495,7 @@ FW_VERSIONS = {
b'68440789AC',
b'68466110AA',
b'68466110AB',
+ b'68466113AA',
b'68469901AA',
b'68469907AA',
b'68522583AA',
@@ -552,6 +570,7 @@ FW_VERSIONS = {
b'68539651AD',
b'68586101AA ',
b'68586105AB ',
+ b'68629919AC ',
b'68629922AC ',
b'68629925AC ',
b'68629926AC ',
@@ -589,6 +608,8 @@ FW_VERSIONS = {
b'68520870AC',
b'68540431AB',
b'68540433AB',
+ b'68551676AA',
+ b'68629935AB',
b'68629936AC',
],
},
diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py
index b66d2a16e0..971338e9b5 100644
--- a/selfdrive/car/docs_definitions.py
+++ b/selfdrive/car/docs_definitions.py
@@ -275,7 +275,7 @@ class CarDocs:
self.min_enable_speed = CP.minEnableSpeed
if self.auto_resume is None:
- self.auto_resume = CP.autoResumeSng
+ self.auto_resume = CP.autoResumeSng and self.min_enable_speed <= 0
# hardware column
hardware_col = "None"
diff --git a/selfdrive/car/ford/carcontroller.py b/selfdrive/car/ford/carcontroller.py
index 47082fb56f..7be3b2ebe9 100644
--- a/selfdrive/car/ford/carcontroller.py
+++ b/selfdrive/car/ford/carcontroller.py
@@ -113,7 +113,7 @@ class CarController(CarControllerBase):
self.steer_alert_last = steer_alert
self.lead_distance_bars_last = hud_control.leadDistanceBars
- new_actuators = actuators.copy()
+ new_actuators = actuators.as_builder()
new_actuators.curvature = self.apply_curvature_last
self.frame += 1
diff --git a/selfdrive/car/ford/interface.py b/selfdrive/car/ford/interface.py
index 7dca458083..2ef5d427e6 100644
--- a/selfdrive/car/ford/interface.py
+++ b/selfdrive/car/ford/interface.py
@@ -39,6 +39,18 @@ class CarInterface(CarInterfaceBase):
if ret.flags & FordFlags.CANFD:
ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_FORD_CANFD
+ else:
+ # Lock out if the car does not have needed lateral and longitudinal control APIs.
+ # Note that we also check CAN for adaptive cruise, but no known signal for LCA exists
+ pscm_config = next((fw for fw in car_fw if fw.ecu == Ecu.eps and b'\x22\xDE\x01' in fw.request), None)
+ if pscm_config:
+ if len(pscm_config.fwVersion) != 24:
+ ret.dashcamOnly = True
+ else:
+ config_tja = pscm_config.fwVersion[7] # Traffic Jam Assist
+ config_lca = pscm_config.fwVersion[8] # Lane Centering Assist
+ if config_tja != 0xFF or config_lca != 0xFF:
+ ret.dashcamOnly = True
# Auto Transmission: 0x732 ECU or Gear_Shift_by_Wire_FD1
found_ecus = [fw.ecu for fw in car_fw]
diff --git a/selfdrive/car/ford/tests/test_ford.py b/selfdrive/car/ford/tests/test_ford.py
index 5d7b2c3332..72dd69980a 100755
--- a/selfdrive/car/ford/tests/test_ford.py
+++ b/selfdrive/car/ford/tests/test_ford.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import random
-import unittest
from collections.abc import Iterable
import capnp
@@ -43,31 +42,31 @@ ECU_PART_NUMBER = {
}
-class TestFordFW(unittest.TestCase):
+class TestFordFW:
def test_fw_query_config(self):
for (ecu, addr, subaddr) in FW_QUERY_CONFIG.extra_ecus:
- self.assertIn(ecu, ECU_ADDRESSES, "Unknown ECU")
- self.assertEqual(addr, ECU_ADDRESSES[ecu], "ECU address mismatch")
- self.assertIsNone(subaddr, "Unexpected ECU subaddress")
+ assert ecu in ECU_ADDRESSES, "Unknown ECU"
+ assert addr == ECU_ADDRESSES[ecu], "ECU address mismatch"
+ assert subaddr is None, "Unexpected ECU subaddress"
@parameterized.expand(FW_VERSIONS.items())
def test_fw_versions(self, car_model: str, fw_versions: dict[tuple[capnp.lib.capnp._EnumModule, int, int | None], Iterable[bytes]]):
for (ecu, addr, subaddr), fws in fw_versions.items():
- self.assertIn(ecu, ECU_PART_NUMBER, "Unexpected ECU")
- self.assertEqual(addr, ECU_ADDRESSES[ecu], "ECU address mismatch")
- self.assertIsNone(subaddr, "Unexpected ECU subaddress")
+ assert ecu in ECU_PART_NUMBER, "Unexpected ECU"
+ assert addr == ECU_ADDRESSES[ecu], "ECU address mismatch"
+ assert subaddr is None, "Unexpected ECU subaddress"
for fw in fws:
- self.assertEqual(len(fw), 24, "Expected ECU response to be 24 bytes")
+ assert len(fw) == 24, "Expected ECU response to be 24 bytes"
match = FW_PATTERN.match(fw)
- self.assertIsNotNone(match, f"Unable to parse FW: {fw!r}")
+ assert match is not None, f"Unable to parse FW: {fw!r}"
if match:
part_number = match.group("part_number")
- self.assertIn(part_number, ECU_PART_NUMBER[ecu], f"Unexpected part number for {fw!r}")
+ assert part_number in ECU_PART_NUMBER[ecu], f"Unexpected part number for {fw!r}"
codes = get_platform_codes([fw])
- self.assertEqual(1, len(codes), f"Unable to parse FW: {fw!r}")
+ assert 1 == len(codes), f"Unable to parse FW: {fw!r}"
@settings(max_examples=100)
@given(data=st.data())
@@ -85,7 +84,7 @@ class TestFordFW(unittest.TestCase):
b"PJ6T-14H102-ABJ\x00\x00\x00\x00\x00\x00\x00\x00\x00",
b"LB5A-14C204-EAC\x00\x00\x00\x00\x00\x00\x00\x00\x00",
])
- self.assertEqual(results, {(b"X6A", b"J"), (b"Z6T", b"N"), (b"J6T", b"P"), (b"B5A", b"L")})
+ assert results == {(b"X6A", b"J"), (b"Z6T", b"N"), (b"J6T", b"P"), (b"B5A", b"L")}
def test_fuzzy_match(self):
for platform, fw_by_addr in FW_VERSIONS.items():
@@ -100,7 +99,7 @@ class TestFordFW(unittest.TestCase):
CP = car.CarParams.new_message(carFw=car_fw)
matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw), CP.carVin, FW_VERSIONS)
- self.assertEqual(matches, {platform})
+ assert matches == {platform}
def test_match_fw_fuzzy(self):
offline_fw = {
@@ -132,18 +131,14 @@ class TestFordFW(unittest.TestCase):
(0x706, None): {b"LB5T-14F397-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
}
candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw})
- self.assertEqual(candidates, {expected_fingerprint})
+ assert candidates == {expected_fingerprint}
# model year hint in between the range should match
live_fw[(0x706, None)] = {b"MB5T-14F397-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"}
candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw,})
- self.assertEqual(candidates, {expected_fingerprint})
+ assert candidates == {expected_fingerprint}
# unseen model year hint should not match
live_fw[(0x760, None)] = {b"M1MC-2D053-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"}
candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw})
- self.assertEqual(len(candidates), 0, "Should not match new model year hint")
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert len(candidates) == 0, "Should not match new model year hint"
diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py
index f2aff2e6f9..fef3c52d34 100755
--- a/selfdrive/car/fw_versions.py
+++ b/selfdrive/car/fw_versions.py
@@ -374,7 +374,7 @@ if __name__ == "__main__":
t = time.time()
print("Getting vin...")
set_obd_multiplexing(params, True)
- vin_rx_addr, vin_rx_bus, vin = get_vin(logcan, sendcan, (0, 1), retry=10, debug=args.debug)
+ vin_rx_addr, vin_rx_bus, vin = get_vin(logcan, sendcan, (0, 1), debug=args.debug)
print(f'RX: {hex(vin_rx_addr)}, BUS: {vin_rx_bus}, VIN: {vin}')
print(f"Getting VIN took {time.time() - t:.3f} s")
print()
diff --git a/selfdrive/car/gm/carcontroller.py b/selfdrive/car/gm/carcontroller.py
index f8d747029b..b204d3b80f 100644
--- a/selfdrive/car/gm/carcontroller.py
+++ b/selfdrive/car/gm/carcontroller.py
@@ -155,7 +155,7 @@ class CarController(CarControllerBase):
if self.frame % 10 == 0:
can_sends.append(gmcan.create_pscm_status(self.packer_pt, CanBus.CAMERA, CS.pscm_status))
- new_actuators = actuators.copy()
+ new_actuators = actuators.as_builder()
new_actuators.steer = self.apply_steer_last / self.params.STEER_MAX
new_actuators.steerOutputCan = self.apply_steer_last
new_actuators.gas = self.apply_gas
diff --git a/selfdrive/car/gm/tests/test_gm.py b/selfdrive/car/gm/tests/test_gm.py
index 01ec8533b8..389d0636c9 100755
--- a/selfdrive/car/gm/tests/test_gm.py
+++ b/selfdrive/car/gm/tests/test_gm.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
from parameterized import parameterized
-import unittest
from openpilot.selfdrive.car.gm.fingerprints import FINGERPRINTS
from openpilot.selfdrive.car.gm.values import CAMERA_ACC_CAR, GM_RX_OFFSET
@@ -8,19 +7,15 @@ from openpilot.selfdrive.car.gm.values import CAMERA_ACC_CAR, GM_RX_OFFSET
CAMERA_DIAGNOSTIC_ADDRESS = 0x24b
-class TestGMFingerprint(unittest.TestCase):
+class TestGMFingerprint:
@parameterized.expand(FINGERPRINTS.items())
def test_can_fingerprints(self, car_model, fingerprints):
- self.assertGreater(len(fingerprints), 0)
+ assert len(fingerprints) > 0
- self.assertTrue(all(len(finger) for finger in fingerprints))
+ assert all(len(finger) for finger in fingerprints)
# The camera can sometimes be communicating on startup
if car_model in CAMERA_ACC_CAR:
for finger in fingerprints:
for required_addr in (CAMERA_DIAGNOSTIC_ADDRESS, CAMERA_DIAGNOSTIC_ADDRESS + GM_RX_OFFSET):
- self.assertEqual(finger.get(required_addr), 8, required_addr)
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert finger.get(required_addr) == 8, required_addr
diff --git a/selfdrive/car/honda/carcontroller.py b/selfdrive/car/honda/carcontroller.py
index 481dd2314e..2e7d3760f7 100644
--- a/selfdrive/car/honda/carcontroller.py
+++ b/selfdrive/car/honda/carcontroller.py
@@ -260,7 +260,7 @@ class CarController(CarControllerBase):
if not self.CP.enableGasInterceptorDEPRECATED:
self.gas = pcm_accel / self.params.NIDEC_GAS_MAX
- new_actuators = actuators.copy()
+ new_actuators = actuators.as_builder()
new_actuators.speed = self.speed
new_actuators.accel = self.accel
new_actuators.gas = self.gas
diff --git a/selfdrive/car/honda/carstate.py b/selfdrive/car/honda/carstate.py
index 17f3b5f1dc..f184d23cd9 100644
--- a/selfdrive/car/honda/carstate.py
+++ b/selfdrive/car/honda/carstate.py
@@ -28,7 +28,7 @@ def get_can_messages(CP, gearbox_msg):
("STEER_MOTOR_TORQUE", 0), # TODO: not on every car
]
- if CP.carFingerprint in (SERIAL_STEERING | (CAR.HONDA_ODYSSEY_CHN, )):
+ if CP.carFingerprint in (SERIAL_STEERING | {CAR.HONDA_ODYSSEY_CHN, }):
messages += [
("SCM_FEEDBACK", 25),
("SCM_BUTTONS", 50),
@@ -58,7 +58,7 @@ def get_can_messages(CP, gearbox_msg):
("ACC_CONTROL", 50),
]
else: # Nidec signals
- if CP.carFingerprint in (SERIAL_STEERING | (CAR.HONDA_ODYSSEY_CHN, )):
+ if CP.carFingerprint in (SERIAL_STEERING | {CAR.HONDA_ODYSSEY_CHN, }):
messages.append(("CRUISE_PARAMS", 10))
else:
messages.append(("CRUISE_PARAMS", 50))
@@ -78,7 +78,7 @@ def get_can_messages(CP, gearbox_msg):
if CP.carFingerprint in HONDA_BOSCH_RADARLESS:
messages.append(("CRUISE_FAULT_STATUS", 50))
- elif CP.carFingerprint == CAR.CLARITY:
+ elif CP.carFingerprint == CAR.HONDA_CLARITY:
messages.append(("BRAKE_ERROR", 100)),
elif CP.openpilotLongitudinalControl:
messages.append(("STANDSTILL", 50))
@@ -149,7 +149,7 @@ class CarState(CarStateBase):
if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS:
ret.accFaulted = bool(cp.vl["CRUISE_FAULT_STATUS"]["CRUISE_FAULT"])
- elif self.CP.carFingerprint == CAR.CLARITY:
+ elif self.CP.carFingerprint == CAR.HONDA_CLARITY:
ret.accFaulted = bool(cp.vl["BRAKE_ERROR"]["BRAKE_ERROR_1"] or cp.vl["BRAKE_ERROR"]["BRAKE_ERROR_2"])
else:
# On some cars, these two signals are always 1, this flag is masking a bug in release
@@ -249,12 +249,12 @@ class CarState(CarStateBase):
if ret.brake > 0.1:
ret.brakePressed = True
- if self.CP.carFingerprint in (CAR.CIVIC, CAR.ODYSSEY, CAR.ODYSSEY_CHN, CAR.CRV_5G, CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH,
- CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E):
+ if self.CP.carFingerprint in (CAR.HONDA_CIVIC, CAR.HONDA_ODYSSEY, CAR.HONDA_ODYSSEY_CHN, CAR.HONDA_CRV_5G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_BOSCH,
+ CAR.HONDA_CIVIC_BOSCH_DIESEL, CAR.HONDA_CRV_HYBRID, CAR.ACURA_RDX_3G, CAR.HONDA_E):
ret.brakeLightsDEPRECATED = bool(cp.vl["ACC_CONTROL"]['BRAKE_LIGHTS'] != 0 or ret.brake > 0.4) if not self.CP.openpilotLongitudinalControl else \
bool(ret.brake > 0.4)
- elif self.CP.carFingerprint in HONDA_BOSCH and self.CP.carFingerprint not in (CAR.CIVIC, CAR.ODYSSEY, CAR.ODYSSEY_CHN, CAR.CRV_5G, CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH,
- CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E) and self.CP.carFingerprint not in HONDA_BOSCH_RADARLESS:
+ elif self.CP.carFingerprint in HONDA_BOSCH and self.CP.carFingerprint not in (CAR.HONDA_CIVIC, CAR.HONDA_ODYSSEY, CAR.HONDA_ODYSSEY_CHN, CAR.HONDA_CRV_5G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_BOSCH,
+ CAR.HONDA_CIVIC_BOSCH_DIESEL, CAR.HONDA_CRV_HYBRID, CAR.ACURA_RDX_3G, CAR.HONDA_E) and self.CP.carFingerprint not in HONDA_BOSCH_RADARLESS:
ret.brakeLightsDEPRECATED = bool(cp.vl["ACC_CONTROL"]['BRAKE_LIGHTS'] != 0 or ret.brake > 0.4) if not self.CP.openpilotLongitudinalControl else \
bool(ret.brake > 0.4)
@@ -263,7 +263,7 @@ class CarState(CarStateBase):
if self.CP.carFingerprint not in HONDA_BOSCH_RADARLESS:
ret.stockAeb = (not self.CP.openpilotLongitudinalControl) and bool(cp.vl["ACC_CONTROL"]["AEB_STATUS"] and cp.vl["ACC_CONTROL"]["ACCEL_COMMAND"] < -1e-5)
else:
- aeb_sig = "COMPUTER_BRAKE_ALT" if self.CP.carFingerprint == CAR.CLARITY else "COMPUTER_BRAKE"
+ aeb_sig = "COMPUTER_BRAKE_ALT" if self.CP.carFingerprint == CAR.HONDA_CLARITY else "COMPUTER_BRAKE"
ret.stockAeb = bool(cp_cam.vl["BRAKE_COMMAND"]["AEB_REQ_1"] and cp_cam.vl["BRAKE_COMMAND"][aeb_sig] > 1e-5)
self.acc_hud = False
diff --git a/selfdrive/car/honda/fingerprints.py b/selfdrive/car/honda/fingerprints.py
index 8c5bdaffc3..a4ac8ce2d4 100644
--- a/selfdrive/car/honda/fingerprints.py
+++ b/selfdrive/car/honda/fingerprints.py
@@ -809,6 +809,7 @@ FW_VERSIONS = {
b'37805-RLV-4070\x00\x00',
b'37805-RLV-5140\x00\x00',
b'37805-RLV-5230\x00\x00',
+ b'37805-RLV-5250\x00\x00',
b'37805-RLV-A630\x00\x00',
b'37805-RLV-A830\x00\x00',
b'37805-RLV-A840\x00\x00',
@@ -833,6 +834,7 @@ FW_VERSIONS = {
b'37805-RLV-L350\x00\x00',
b'37805-RLV-L410\x00\x00',
b'37805-RLV-L430\x00\x00',
+ b'37805-RLV-L830\x00\x00',
b'37805-RLV-L850\x00\x00',
],
(Ecu.gateway, 0x18daeff1, None): [
@@ -921,6 +923,7 @@ FW_VERSIONS = {
b'37805-5YF-A430\x00\x00',
b'37805-5YF-A750\x00\x00',
b'37805-5YF-A760\x00\x00',
+ b'37805-5YF-A770\x00\x00',
b'37805-5YF-A850\x00\x00',
b'37805-5YF-A860\x00\x00',
b'37805-5YF-A870\x00\x00',
diff --git a/selfdrive/car/honda/tests/test_honda.py b/selfdrive/car/honda/tests/test_honda.py
index 60d91b84a8..54d177d2ed 100755
--- a/selfdrive/car/honda/tests/test_honda.py
+++ b/selfdrive/car/honda/tests/test_honda.py
@@ -1,20 +1,15 @@
#!/usr/bin/env python3
import re
-import unittest
from openpilot.selfdrive.car.honda.fingerprints import FW_VERSIONS
HONDA_FW_VERSION_RE = br"[A-Z0-9]{5}-[A-Z0-9]{3}(-|,)[A-Z0-9]{4}(\x00){2}$"
-class TestHondaFingerprint(unittest.TestCase):
+class TestHondaFingerprint:
def test_fw_version_format(self):
# Asserts all FW versions follow an expected format
for fw_by_ecu in FW_VERSIONS.values():
for fws in fw_by_ecu.values():
for fw in fws:
- self.assertTrue(re.match(HONDA_FW_VERSION_RE, fw) is not None, fw)
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert re.match(HONDA_FW_VERSION_RE, fw) is not None, fw
diff --git a/selfdrive/car/hyundai/carcontroller.py b/selfdrive/car/hyundai/carcontroller.py
index b9aa94e909..c59dfaa3c0 100644
--- a/selfdrive/car/hyundai/carcontroller.py
+++ b/selfdrive/car/hyundai/carcontroller.py
@@ -165,7 +165,7 @@ class CarController(CarControllerBase):
if self.frame % 50 == 0 and self.CP.openpilotLongitudinalControl and not escc:
can_sends.append(hyundaican.create_frt_radar_opt(self.packer))
- new_actuators = actuators.copy()
+ new_actuators = actuators.as_builder()
new_actuators.steer = apply_steer / self.params.STEER_MAX
new_actuators.steerOutputCan = apply_steer
new_actuators.accel = accel
diff --git a/selfdrive/car/hyundai/fingerprints.py b/selfdrive/car/hyundai/fingerprints.py
index 2fead28cdb..bba304bfb1 100644
--- a/selfdrive/car/hyundai/fingerprints.py
+++ b/selfdrive/car/hyundai/fingerprints.py
@@ -117,6 +117,7 @@ FW_VERSIONS = {
b'\xf1\x00AEhe SCC FHCUP 1.00 1.02 99110-G2100 ',
],
(Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00AE MDPS C 1.00 1.01 56310/G2210 4APHC101',
b'\xf1\x00AE MDPS C 1.00 1.01 56310/G2310 4APHC101',
b'\xf1\x00AE MDPS C 1.00 1.01 56310/G2510 4APHC101',
b'\xf1\x00AE MDPS C 1.00 1.01 56310/G2560 4APHC101',
@@ -864,6 +865,7 @@ FW_VERSIONS = {
b'\xf1\x00CN7_ SCC FNCUP 1.00 1.01 99110-AA000 ',
],
(Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00CN7 MDPS C 1.00 1.06 56310/AA050 4CNDC106',
b'\xf1\x00CN7 MDPS C 1.00 1.06 56310/AA070 4CNDC106',
b'\xf1\x00CN7 MDPS C 1.00 1.06 56310AA050\x00 4CNDC106',
b'\xf1\x00CN7 MDPS C 1.00 1.07 56310AA050\x00 4CNDC107',
@@ -965,6 +967,7 @@ FW_VERSIONS = {
b'\xf1\x00CV1 MFC AT KOR LHD 1.00 1.05 99210-CV000 211027',
b'\xf1\x00CV1 MFC AT KOR LHD 1.00 1.06 99210-CV000 220328',
b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.00 99210-CV100 220630',
+ b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.00 99210-CV200 230510',
b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.05 99210-CV000 211027',
b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.06 99210-CV000 220328',
],
@@ -974,6 +977,7 @@ FW_VERSIONS = {
b'\xf1\x00NE1_ RDR ----- 1.00 1.00 99110-GI000 ',
],
(Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00NE1 MFC AT CAN LHD 1.00 1.01 99211-GI010 211007',
b'\xf1\x00NE1 MFC AT CAN LHD 1.00 1.05 99211-GI010 220614',
b'\xf1\x00NE1 MFC AT EUR LHD 1.00 1.01 99211-GI010 211007',
b'\xf1\x00NE1 MFC AT EUR LHD 1.00 1.06 99211-GI000 210813',
@@ -1002,6 +1006,7 @@ FW_VERSIONS = {
},
CAR.HYUNDAI_TUCSON_4TH_GEN: {
(Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00NX4 FR_CMR AT CAN LHD 1.00 1.00 99211-N9260 14Y',
b'\xf1\x00NX4 FR_CMR AT CAN LHD 1.00 1.01 99211-N9100 14A',
b'\xf1\x00NX4 FR_CMR AT EUR LHD 1.00 1.00 99211-N9220 14K',
b'\xf1\x00NX4 FR_CMR AT EUR LHD 1.00 2.02 99211-N9000 14E',
@@ -1037,6 +1042,7 @@ FW_VERSIONS = {
b'\xf1\x00NQ5 FR_CMR AT GEN LHD 1.00 1.00 99211-P1060 665',
b'\xf1\x00NQ5 FR_CMR AT USA LHD 1.00 1.00 99211-P1030 662',
b'\xf1\x00NQ5 FR_CMR AT USA LHD 1.00 1.00 99211-P1040 663',
+ b'\xf1\x00NQ5 FR_CMR AT EUR LHD 1.00 1.00 99211-P1040 663',
b'\xf1\x00NQ5 FR_CMR AT USA LHD 1.00 1.00 99211-P1060 665',
b'\xf1\x00NQ5 FR_CMR AT USA LHD 1.00 1.00 99211-P1070 690',
],
diff --git a/selfdrive/car/hyundai/tests/test_hyundai.py b/selfdrive/car/hyundai/tests/test_hyundai.py
index 0753b372e1..db2110b0de 100755
--- a/selfdrive/car/hyundai/tests/test_hyundai.py
+++ b/selfdrive/car/hyundai/tests/test_hyundai.py
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
from hypothesis import settings, given, strategies as st
-import unittest
+
+import pytest
from cereal import car
from openpilot.selfdrive.car.fw_versions import build_fw_dict
@@ -39,19 +40,19 @@ NO_DATES_PLATFORMS = {
CANFD_EXPECTED_ECUS = {Ecu.fwdCamera, Ecu.fwdRadar}
-class TestHyundaiFingerprint(unittest.TestCase):
+class TestHyundaiFingerprint:
def test_can_features(self):
# Test no EV/HEV in any gear lists (should all use ELECT_GEAR)
- self.assertEqual(set.union(*CAN_GEARS.values()) & (HYBRID_CAR | EV_CAR), set())
+ assert set.union(*CAN_GEARS.values()) & (HYBRID_CAR | EV_CAR) == set()
# Test CAN FD car not in CAN feature lists
can_specific_feature_list = set.union(*CAN_GEARS.values(), *CHECKSUM.values(), LEGACY_SAFETY_MODE_CAR, UNSUPPORTED_LONGITUDINAL_CAR, CAMERA_SCC_CAR)
for car_model in CANFD_CAR:
- self.assertNotIn(car_model, can_specific_feature_list, "CAN FD car unexpectedly found in a CAN feature list")
+ assert car_model not in can_specific_feature_list, "CAN FD car unexpectedly found in a CAN feature list"
def test_hybrid_ev_sets(self):
- self.assertEqual(HYBRID_CAR & EV_CAR, set(), "Shared cars between hybrid and EV")
- self.assertEqual(CANFD_CAR & HYBRID_CAR, set(), "Hard coding CAN FD cars as hybrid is no longer supported")
+ assert HYBRID_CAR & EV_CAR == set(), "Shared cars between hybrid and EV"
+ assert CANFD_CAR & HYBRID_CAR == set(), "Hard coding CAN FD cars as hybrid is no longer supported"
def test_canfd_ecu_whitelist(self):
# Asserts only expected Ecus can exist in database for CAN-FD cars
@@ -59,34 +60,34 @@ class TestHyundaiFingerprint(unittest.TestCase):
ecus = {fw[0] for fw in FW_VERSIONS[car_model].keys()}
ecus_not_in_whitelist = ecus - CANFD_EXPECTED_ECUS
ecu_strings = ", ".join([f"Ecu.{ECU_NAME[ecu]}" for ecu in ecus_not_in_whitelist])
- self.assertEqual(len(ecus_not_in_whitelist), 0,
- f"{car_model}: Car model has unexpected ECUs: {ecu_strings}")
+ assert len(ecus_not_in_whitelist) == 0, \
+ f"{car_model}: Car model has unexpected ECUs: {ecu_strings}"
- def test_blacklisted_parts(self):
+ def test_blacklisted_parts(self, subtests):
# Asserts no ECUs known to be shared across platforms exist in the database.
# Tucson having Santa Cruz camera and EPS for example
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
if car_model == CAR.HYUNDAI_SANTA_CRUZ_1ST_GEN:
- raise unittest.SkipTest("Skip checking Santa Cruz for its parts")
+ pytest.skip("Skip checking Santa Cruz for its parts")
for code, _ in get_platform_codes(ecus[(Ecu.fwdCamera, 0x7c4, None)]):
if b"-" not in code:
continue
part = code.split(b"-")[1]
- self.assertFalse(part.startswith(b'CW'), "Car has bad part number")
+ assert not part.startswith(b'CW'), "Car has bad part number"
- def test_correct_ecu_response_database(self):
+ def test_correct_ecu_response_database(self, subtests):
"""
Assert standard responses for certain ECUs, since they can
respond to multiple queries with different data
"""
expected_fw_prefix = HYUNDAI_VERSION_REQUEST_LONG[1:]
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
for ecu, fws in ecus.items():
- self.assertTrue(all(fw.startswith(expected_fw_prefix) for fw in fws),
- f"FW from unexpected request in database: {(ecu, fws)}")
+ assert all(fw.startswith(expected_fw_prefix) for fw in fws), \
+ f"FW from unexpected request in database: {(ecu, fws)}"
@settings(max_examples=100)
@given(data=st.data())
@@ -96,10 +97,10 @@ class TestHyundaiFingerprint(unittest.TestCase):
fws = data.draw(fw_strategy)
get_platform_codes(fws)
- def test_expected_platform_codes(self):
+ def test_expected_platform_codes(self, subtests):
# Ensures we don't accidentally add multiple platform codes for a car unless it is intentional
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
for ecu, fws in ecus.items():
if ecu[0] not in PLATFORM_CODE_ECUS:
continue
@@ -107,37 +108,37 @@ class TestHyundaiFingerprint(unittest.TestCase):
# Third and fourth character are usually EV/hybrid identifiers
codes = {code.split(b"-")[0][:2] for code, _ in get_platform_codes(fws)}
if car_model == CAR.HYUNDAI_PALISADE:
- self.assertEqual(codes, {b"LX", b"ON"}, f"Car has unexpected platform codes: {car_model} {codes}")
+ assert codes == {b"LX", b"ON"}, f"Car has unexpected platform codes: {car_model} {codes}"
elif car_model == CAR.HYUNDAI_KONA_EV and ecu[0] == Ecu.fwdCamera:
- self.assertEqual(codes, {b"OE", b"OS"}, f"Car has unexpected platform codes: {car_model} {codes}")
+ assert codes == {b"OE", b"OS"}, f"Car has unexpected platform codes: {car_model} {codes}"
else:
- self.assertEqual(len(codes), 1, f"Car has multiple platform codes: {car_model} {codes}")
+ assert len(codes) == 1, f"Car has multiple platform codes: {car_model} {codes}"
# Tests for platform codes, part numbers, and FW dates which Hyundai will use to fuzzy
# fingerprint in the absence of full FW matches:
- def test_platform_code_ecus_available(self):
+ def test_platform_code_ecus_available(self, subtests):
# TODO: add queries for these non-CAN FD cars to get EPS
no_eps_platforms = CANFD_CAR | {CAR.KIA_SORENTO, CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.KIA_OPTIMA_H,
CAR.KIA_OPTIMA_H_G4_FL, CAR.HYUNDAI_SONATA_LF, CAR.HYUNDAI_TUCSON, CAR.GENESIS_G90, CAR.GENESIS_G80, CAR.HYUNDAI_ELANTRA}
# Asserts ECU keys essential for fuzzy fingerprinting are available on all platforms
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
for platform_code_ecu in PLATFORM_CODE_ECUS:
if platform_code_ecu in (Ecu.fwdRadar, Ecu.eps) and car_model == CAR.HYUNDAI_GENESIS:
continue
if platform_code_ecu == Ecu.eps and car_model in no_eps_platforms:
continue
- self.assertIn(platform_code_ecu, [e[0] for e in ecus])
+ assert platform_code_ecu in [e[0] for e in ecus]
- def test_fw_format(self):
+ def test_fw_format(self, subtests):
# Asserts:
# - every supported ECU FW version returns one platform code
# - every supported ECU FW version has a part number
# - expected parsing of ECU FW dates
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
for ecu, fws in ecus.items():
if ecu[0] not in PLATFORM_CODE_ECUS:
continue
@@ -145,40 +146,40 @@ class TestHyundaiFingerprint(unittest.TestCase):
codes = set()
for fw in fws:
result = get_platform_codes([fw])
- self.assertEqual(1, len(result), f"Unable to parse FW: {fw}")
+ assert 1 == len(result), f"Unable to parse FW: {fw}"
codes |= result
if ecu[0] not in DATE_FW_ECUS or car_model in NO_DATES_PLATFORMS:
- self.assertTrue(all(date is None for _, date in codes))
+ assert all(date is None for _, date in codes)
else:
- self.assertTrue(all(date is not None for _, date in codes))
+ assert all(date is not None for _, date in codes)
if car_model == CAR.HYUNDAI_GENESIS:
- raise unittest.SkipTest("No part numbers for car model")
+ pytest.skip("No part numbers for car model")
# Hyundai places the ECU part number in their FW versions, assert all parsable
# Some examples of valid formats: b"56310-L0010", b"56310L0010", b"56310/M6300"
- self.assertTrue(all(b"-" in code for code, _ in codes),
- f"FW does not have part number: {fw}")
+ assert all(b"-" in code for code, _ in codes), \
+ f"FW does not have part number: {fw}"
def test_platform_codes_spot_check(self):
# Asserts basic platform code parsing behavior for a few cases
results = get_platform_codes([b"\xf1\x00DH LKAS 1.1 -150210"])
- self.assertEqual(results, {(b"DH", b"150210")})
+ assert results == {(b"DH", b"150210")}
# Some cameras and all radars do not have dates
results = get_platform_codes([b"\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2000 "])
- self.assertEqual(results, {(b"AEhe-G2000", None)})
+ assert results == {(b"AEhe-G2000", None)}
results = get_platform_codes([b"\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 "])
- self.assertEqual(results, {(b"CV1-CV000", None)})
+ assert results == {(b"CV1-CV000", None)}
results = get_platform_codes([
b"\xf1\x00DH LKAS 1.1 -150210",
b"\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2000 ",
b"\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 ",
])
- self.assertEqual(results, {(b"DH", b"150210"), (b"AEhe-G2000", None), (b"CV1-CV000", None)})
+ assert results == {(b"DH", b"150210"), (b"AEhe-G2000", None), (b"CV1-CV000", None)}
results = get_platform_codes([
b"\xf1\x00LX2 MFC AT USA LHD 1.00 1.07 99211-S8100 220222",
@@ -186,8 +187,8 @@ class TestHyundaiFingerprint(unittest.TestCase):
b"\xf1\x00ON MFC AT USA LHD 1.00 1.01 99211-S9100 190405",
b"\xf1\x00ON MFC AT USA LHD 1.00 1.03 99211-S9100 190720",
])
- self.assertEqual(results, {(b"LX2-S8100", b"220222"), (b"LX2-S8100", b"211103"),
- (b"ON-S9100", b"190405"), (b"ON-S9100", b"190720")})
+ assert results == {(b"LX2-S8100", b"220222"), (b"LX2-S8100", b"211103"),
+ (b"ON-S9100", b"190405"), (b"ON-S9100", b"190720")}
def test_fuzzy_excluded_platforms(self):
# Asserts a list of platforms that will not fuzzy fingerprint with platform codes due to them being shared.
@@ -211,12 +212,8 @@ class TestHyundaiFingerprint(unittest.TestCase):
CP = car.CarParams.new_message(carFw=car_fw)
matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw), CP.carVin, FW_VERSIONS)
if len(matches) == 1:
- self.assertEqual(list(matches)[0], platform)
+ assert list(matches)[0] == platform
else:
platforms_with_shared_codes.add(platform)
- self.assertEqual(platforms_with_shared_codes, excluded_platforms)
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert platforms_with_shared_codes == excluded_platforms
diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py
index a62a6d8143..ad40db7bd5 100644
--- a/selfdrive/car/hyundai/values.py
+++ b/selfdrive/car/hyundai/values.py
@@ -225,7 +225,7 @@ class CAR(Platforms):
flags=HyundaiFlags.HYBRID,
)
HYUNDAI_KONA = HyundaiPlatformConfig(
- [HyundaiCarDocs("Hyundai Kona 2020", car_parts=CarParts.common([CarHarness.hyundai_b]))],
+ [HyundaiCarDocs("Hyundai Kona 2020", min_enable_speed=6 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_b]))],
CarSpecs(mass=1275, wheelbase=2.6, steerRatio=13.42, tireStiffnessFactor=0.385),
flags=HyundaiFlags.CLUSTER_GEARS,
)
@@ -333,6 +333,7 @@ class CAR(Platforms):
HyundaiCarDocs("Hyundai Tucson 2022", car_parts=CarParts.common([CarHarness.hyundai_n])),
HyundaiCarDocs("Hyundai Tucson 2023-24", "All", car_parts=CarParts.common([CarHarness.hyundai_n])),
HyundaiCarDocs("Hyundai Tucson Hybrid 2022-24", "All", car_parts=CarParts.common([CarHarness.hyundai_n])),
+ HyundaiCarDocs("Hyundai Tucson Plug-in Hybrid 2024", "All", car_parts=CarParts.common([CarHarness.hyundai_n])),
],
CarSpecs(mass=1630, wheelbase=2.756, steerRatio=13.7, tireStiffnessFactor=0.385),
)
@@ -350,8 +351,8 @@ class CAR(Platforms):
# Kia
KIA_FORTE = HyundaiPlatformConfig(
[
- HyundaiCarDocs("Kia Forte 2019-21", car_parts=CarParts.common([CarHarness.hyundai_g])),
- HyundaiCarDocs("Kia Forte 2023", car_parts=CarParts.common([CarHarness.hyundai_e])),
+ HyundaiCarDocs("Kia Forte 2019-21", min_enable_speed=6 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_g])),
+ HyundaiCarDocs("Kia Forte 2022-23", car_parts=CarParts.common([CarHarness.hyundai_e])),
],
CarSpecs(mass=2878 * CV.LB_TO_KG, wheelbase=2.8, steerRatio=13.75, tireStiffnessFactor=0.5)
)
@@ -487,9 +488,9 @@ class CAR(Platforms):
)
KIA_EV6 = HyundaiCanFDPlatformConfig(
[
- HyundaiCarDocs("Kia EV6 (Southeast Asia only) 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_p])),
- HyundaiCarDocs("Kia EV6 (without HDA II) 2022-23", "Highway Driving Assist", car_parts=CarParts.common([CarHarness.hyundai_l])),
- HyundaiCarDocs("Kia EV6 (with HDA II) 2022-23", "Highway Driving Assist II", car_parts=CarParts.common([CarHarness.hyundai_p]))
+ HyundaiCarDocs("Kia EV6 (Southeast Asia only) 2022-24", "All", car_parts=CarParts.common([CarHarness.hyundai_p])),
+ HyundaiCarDocs("Kia EV6 (without HDA II) 2022-24", "Highway Driving Assist", car_parts=CarParts.common([CarHarness.hyundai_l])),
+ HyundaiCarDocs("Kia EV6 (with HDA II) 2022-24", "Highway Driving Assist II", car_parts=CarParts.common([CarHarness.hyundai_p]))
],
CarSpecs(mass=2055, wheelbase=2.9, steerRatio=16, tireStiffnessFactor=0.65),
flags=HyundaiFlags.EV,
@@ -692,6 +693,8 @@ PLATFORM_CODE_ECUS = [Ecu.fwdRadar, Ecu.fwdCamera, Ecu.eps]
# TODO: there are date codes in the ABS firmware versions in hex
DATE_FW_ECUS = [Ecu.fwdCamera]
+# Note: an ECU on CAN FD cars may sometimes send 0x30080aaaaaaaaaaa (flow control continue) while we
+# are attempting to query ECUs. This currently does not seem to affect fingerprinting from the camera
FW_QUERY_CONFIG = FwQueryConfig(
requests=[
# TODO: add back whitelists
diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py
index 34a1d8ea75..9538987ad1 100644
--- a/selfdrive/car/interfaces.py
+++ b/selfdrive/car/interfaces.py
@@ -6,6 +6,7 @@ from abc import abstractmethod, ABC
from enum import StrEnum
from typing import Any, NamedTuple
from collections.abc import Callable
+from functools import cache
from cereal import car
from openpilot.common.basedir import BASEDIR
@@ -43,6 +44,7 @@ class LatControlInputs(NamedTuple):
TorqueFromLateralAccelCallbackType = Callable[[LatControlInputs, car.CarParams.LateralTorqueTuning, float, float, bool, bool], float]
+@cache
def get_torque_params(candidate):
with open(TORQUE_SUBSTITUTE_PATH, 'rb') as f:
sub = tomllib.load(f)
@@ -243,11 +245,10 @@ class CarInterfaceBase(ABC):
ret.cruiseState.speedCluster = ret.cruiseState.speed
# copy back for next iteration
- reader = ret.as_reader()
if self.CS is not None:
- self.CS.out = reader
+ self.CS.out = ret.as_reader()
- return reader
+ return ret
def create_common_events(self, cs_out, extra_gears=None, pcm_enable=True, allow_enable=True,
@@ -283,6 +284,10 @@ class CarInterfaceBase(ABC):
events.add(EventName.accFaulted)
if cs_out.steeringPressed:
events.add(EventName.steerOverride)
+ if cs_out.brakePressed and cs_out.standstill:
+ events.add(EventName.preEnableStandstill)
+ if cs_out.gasPressed:
+ events.add(EventName.gasPressedOverride)
# Handle button presses
for b in cs_out.buttonEvents:
diff --git a/selfdrive/car/mazda/carcontroller.py b/selfdrive/car/mazda/carcontroller.py
index 8d3a59b4ea..3d41634879 100644
--- a/selfdrive/car/mazda/carcontroller.py
+++ b/selfdrive/car/mazda/carcontroller.py
@@ -58,7 +58,7 @@ class CarController(CarControllerBase):
can_sends.append(mazdacan.create_steering_control(self.packer, self.CP,
self.frame, apply_steer, CS.cam_lkas))
- new_actuators = CC.actuators.copy()
+ new_actuators = CC.actuators.as_builder()
new_actuators.steer = apply_steer / CarControllerParams.STEER_MAX
new_actuators.steerOutputCan = apply_steer
diff --git a/selfdrive/car/mock/carcontroller.py b/selfdrive/car/mock/carcontroller.py
index 2b2da954ff..0cd37c0369 100644
--- a/selfdrive/car/mock/carcontroller.py
+++ b/selfdrive/car/mock/carcontroller.py
@@ -2,4 +2,4 @@ from openpilot.selfdrive.car.interfaces import CarControllerBase
class CarController(CarControllerBase):
def update(self, CC, CS, now_nanos):
- return CC.actuators.copy(), []
+ return CC.actuators.as_builder(), []
diff --git a/selfdrive/car/nissan/carcontroller.py b/selfdrive/car/nissan/carcontroller.py
index 83775462b7..c7bd231398 100644
--- a/selfdrive/car/nissan/carcontroller.py
+++ b/selfdrive/car/nissan/carcontroller.py
@@ -75,7 +75,7 @@ class CarController(CarControllerBase):
self.packer, CS.lkas_hud_info_msg, steer_hud_alert
))
- new_actuators = actuators.copy()
+ new_actuators = actuators.as_builder()
new_actuators.steeringAngleDeg = apply_angle
self.frame += 1
diff --git a/selfdrive/car/subaru/carcontroller.py b/selfdrive/car/subaru/carcontroller.py
index 22a1475b5b..d89ae8c639 100644
--- a/selfdrive/car/subaru/carcontroller.py
+++ b/selfdrive/car/subaru/carcontroller.py
@@ -136,7 +136,7 @@ class CarController(CarControllerBase):
if self.frame % 2 == 0:
can_sends.append(subarucan.create_es_static_2(self.packer))
- new_actuators = actuators.copy()
+ new_actuators = actuators.as_builder()
new_actuators.steer = self.apply_steer_last / self.p.STEER_MAX
new_actuators.steerOutputCan = self.apply_steer_last
diff --git a/selfdrive/car/subaru/fingerprints.py b/selfdrive/car/subaru/fingerprints.py
index 41727d7623..e3bd9d9099 100644
--- a/selfdrive/car/subaru/fingerprints.py
+++ b/selfdrive/car/subaru/fingerprints.py
@@ -164,6 +164,7 @@ FW_VERSIONS = {
b'\xa2 \x194\x00',
b'\xa2 `\x00',
b'\xa2 !3\x00',
+ b'\xa2 !6\x00',
b'\xa2 !`\x00',
b'\xa2 !i\x00',
],
@@ -172,6 +173,7 @@ FW_VERSIONS = {
b'\n\xc0\x04\x01',
b'\x9a\xc0\x00\x00',
b'\x9a\xc0\x04\x00',
+ b'\x9a\xc0\n\x01',
],
(Ecu.fwdCamera, 0x787, None): [
b'\x00\x00eb\x1f@ "',
@@ -181,6 +183,7 @@ FW_VERSIONS = {
b'\x00\x00e\x8f\x1f@ )',
b'\x00\x00e\x92\x00\x00\x00\x00',
b'\x00\x00e\xa4\x00\x00\x00\x00',
+ b'\x00\x00e\xa4\x1f@ (',
],
(Ecu.engine, 0x7e0, None): [
b'\xca!`0\x07',
@@ -188,6 +191,7 @@ FW_VERSIONS = {
b'\xca!ap\x07',
b'\xca!f@\x07',
b'\xca!fp\x07',
+ b'\xcaacp\x07',
b'\xcc!`p\x07',
b'\xcc!fp\x07',
b'\xcc"f0\x07',
@@ -200,6 +204,7 @@ FW_VERSIONS = {
b'\xf3"fr\x07',
],
(Ecu.transmission, 0x7e1, None): [
+ b'\xe6\x15\x042\x00',
b'\xe6\xf5\x04\x00\x00',
b'\xe6\xf5$\x00\x00',
b'\xe6\xf5D0\x00',
diff --git a/selfdrive/car/subaru/tests/test_subaru.py b/selfdrive/car/subaru/tests/test_subaru.py
index c8cdf66065..33040442b6 100644
--- a/selfdrive/car/subaru/tests/test_subaru.py
+++ b/selfdrive/car/subaru/tests/test_subaru.py
@@ -1,5 +1,4 @@
from cereal import car
-import unittest
from openpilot.selfdrive.car.subaru.fingerprints import FW_VERSIONS
Ecu = car.CarParams.Ecu
@@ -7,14 +6,11 @@ Ecu = car.CarParams.Ecu
ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()}
-class TestSubaruFingerprint(unittest.TestCase):
+class TestSubaruFingerprint:
def test_fw_version_format(self):
for platform, fws_per_ecu in FW_VERSIONS.items():
for (ecu, _, _), fws in fws_per_ecu.items():
fw_size = len(fws[0])
for fw in fws:
- self.assertEqual(len(fw), fw_size, f"{platform} {ecu}: {len(fw)} {fw_size}")
+ assert len(fw) == fw_size, f"{platform} {ecu}: {len(fw)} {fw_size}"
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/car/tesla/carcontroller.py b/selfdrive/car/tesla/carcontroller.py
index f217c4692d..e460111e32 100644
--- a/selfdrive/car/tesla/carcontroller.py
+++ b/selfdrive/car/tesla/carcontroller.py
@@ -60,7 +60,7 @@ class CarController(CarControllerBase):
# TODO: HUD control
- new_actuators = actuators.copy()
+ new_actuators = actuators.as_builder()
new_actuators.steeringAngleDeg = self.apply_angle_last
self.frame += 1
diff --git a/selfdrive/car/tests/test_can_fingerprint.py b/selfdrive/car/tests/test_can_fingerprint.py
index 8df7007339..bb585d567f 100755
--- a/selfdrive/car/tests/test_can_fingerprint.py
+++ b/selfdrive/car/tests/test_can_fingerprint.py
@@ -1,13 +1,12 @@
#!/usr/bin/env python3
from parameterized import parameterized
-import unittest
from cereal import log, messaging
from openpilot.selfdrive.car.car_helpers import FRAME_FINGERPRINT, can_fingerprint
from openpilot.selfdrive.car.fingerprints import _FINGERPRINTS as FINGERPRINTS
-class TestCanFingerprint(unittest.TestCase):
+class TestCanFingerprint:
@parameterized.expand(list(FINGERPRINTS.items()))
def test_can_fingerprint(self, car_model, fingerprints):
"""Tests online fingerprinting function on offline fingerprints"""
@@ -21,12 +20,12 @@ class TestCanFingerprint(unittest.TestCase):
empty_can = messaging.new_message('can', 0)
car_fingerprint, finger = can_fingerprint(lambda: next(fingerprint_iter, empty_can)) # noqa: B023
- self.assertEqual(car_fingerprint, car_model)
- self.assertEqual(finger[0], fingerprint)
- self.assertEqual(finger[1], fingerprint)
- self.assertEqual(finger[2], {})
+ assert car_fingerprint == car_model
+ assert finger[0] == fingerprint
+ assert finger[1] == fingerprint
+ assert finger[2] == {}
- def test_timing(self):
+ def test_timing(self, subtests):
# just pick any CAN fingerprinting car
car_model = "CHEVROLET_BOLT_EUV"
fingerprint = FINGERPRINTS[car_model][0]
@@ -50,7 +49,7 @@ class TestCanFingerprint(unittest.TestCase):
cases.append((FRAME_FINGERPRINT * 2, None, can))
for expected_frames, car_model, can in cases:
- with self.subTest(expected_frames=expected_frames, car_model=car_model):
+ with subtests.test(expected_frames=expected_frames, car_model=car_model):
frames = 0
def test():
@@ -59,9 +58,5 @@ class TestCanFingerprint(unittest.TestCase):
return can # noqa: B023
car_fingerprint, _ = can_fingerprint(test)
- self.assertEqual(car_fingerprint, car_model)
- self.assertEqual(frames, expected_frames + 2) # TODO: fix extra frames
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert car_fingerprint == car_model
+ assert frames == expected_frames + 2# TODO: fix extra frames
diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py
index ce5b186388..d16294cf84 100755
--- a/selfdrive/car/tests/test_car_interfaces.py
+++ b/selfdrive/car/tests/test_car_interfaces.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import os
import math
-import unittest
import hypothesis.strategies as st
from hypothesis import Phase, given, settings
import importlib
@@ -12,7 +11,7 @@ from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.car import gen_empty_fingerprint
from openpilot.selfdrive.car.car_helpers import interfaces
from openpilot.selfdrive.car.fingerprints import all_known_cars
-from openpilot.selfdrive.car.fw_versions import FW_VERSIONS
+from openpilot.selfdrive.car.fw_versions import FW_VERSIONS, FW_QUERY_CONFIGS
from openpilot.selfdrive.car.interfaces import get_interface_attr
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
@@ -20,7 +19,10 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
from openpilot.selfdrive.controls.lib.longcontrol import LongControl
from openpilot.selfdrive.test.fuzzy_generation import DrawType, FuzzyGenerator
-ALL_ECUS = list({ecu for ecus in FW_VERSIONS.values() for ecu in ecus.keys()})
+ALL_ECUS = {ecu for ecus in FW_VERSIONS.values() for ecu in ecus.keys()}
+ALL_ECUS |= {ecu for config in FW_QUERY_CONFIGS.values() for ecu in config.extra_ecus}
+
+ALL_REQUESTS = {tuple(r.request) for config in FW_QUERY_CONFIGS.values() for r in config.requests}
MAX_EXAMPLES = int(os.environ.get('MAX_EXAMPLES', '40'))
@@ -32,7 +34,7 @@ def get_fuzzy_car_interface_args(draw: DrawType) -> dict:
gen_empty_fingerprint()})
# only pick from possible ecus to reduce search space
- car_fw_strategy = st.lists(st.sampled_from(ALL_ECUS))
+ car_fw_strategy = st.lists(st.sampled_from(sorted(ALL_ECUS)))
params_strategy = st.fixed_dictionaries({
'fingerprints': fingerprint_strategy,
@@ -41,11 +43,13 @@ def get_fuzzy_car_interface_args(draw: DrawType) -> dict:
})
params: dict = draw(params_strategy)
- params['car_fw'] = [car.CarParams.CarFw(ecu=fw[0], address=fw[1], subAddress=fw[2] or 0) for fw in params['car_fw']]
+ params['car_fw'] = [car.CarParams.CarFw(ecu=fw[0], address=fw[1], subAddress=fw[2] or 0,
+ request=draw(st.sampled_from(sorted(ALL_REQUESTS))))
+ for fw in params['car_fw']]
return params
-class TestCarInterfaces(unittest.TestCase):
+class TestCarInterfaces:
# FIXME: Due to the lists used in carParams, Phase.target is very slow and will cause
# many generated examples to overrun when max_examples > ~20, don't use it
@parameterized.expand([(car,) for car in sorted(all_known_cars())])
@@ -63,16 +67,16 @@ class TestCarInterfaces(unittest.TestCase):
assert car_params
assert car_interface
- self.assertGreater(car_params.mass, 1)
- self.assertGreater(car_params.wheelbase, 0)
+ assert car_params.mass > 1
+ assert car_params.wheelbase > 0
# centerToFront is center of gravity to front wheels, assert a reasonable range
- self.assertTrue(car_params.wheelbase * 0.3 < car_params.centerToFront < car_params.wheelbase * 0.7)
- self.assertGreater(car_params.maxLateralAccel, 0)
+ assert car_params.wheelbase * 0.3 < car_params.centerToFront < car_params.wheelbase * 0.7
+ assert car_params.maxLateralAccel > 0
# Longitudinal sanity checks
- self.assertEqual(len(car_params.longitudinalTuning.kpV), len(car_params.longitudinalTuning.kpBP))
- self.assertEqual(len(car_params.longitudinalTuning.kiV), len(car_params.longitudinalTuning.kiBP))
- self.assertEqual(len(car_params.longitudinalTuning.deadzoneV), len(car_params.longitudinalTuning.deadzoneBP))
+ assert len(car_params.longitudinalTuning.kpV) == len(car_params.longitudinalTuning.kpBP)
+ assert len(car_params.longitudinalTuning.kiV) == len(car_params.longitudinalTuning.kiBP)
+ assert len(car_params.longitudinalTuning.deadzoneV) == len(car_params.longitudinalTuning.deadzoneBP)
# If we're using the interceptor for gasPressed, we should be commanding gas with it
if car_params.enableGasInterceptorDEPRECATED:
@@ -82,13 +86,13 @@ class TestCarInterfaces(unittest.TestCase):
if car_params.steerControlType != car.CarParams.SteerControlType.angle:
tune = car_params.lateralTuning
if tune.which() == 'pid':
- self.assertTrue(not math.isnan(tune.pid.kf) and tune.pid.kf > 0)
- self.assertTrue(len(tune.pid.kpV) > 0 and len(tune.pid.kpV) == len(tune.pid.kpBP))
- self.assertTrue(len(tune.pid.kiV) > 0 and len(tune.pid.kiV) == len(tune.pid.kiBP))
+ assert not math.isnan(tune.pid.kf) and tune.pid.kf > 0
+ assert len(tune.pid.kpV) > 0 and len(tune.pid.kpV) == len(tune.pid.kpBP)
+ assert len(tune.pid.kiV) > 0 and len(tune.pid.kiV) == len(tune.pid.kiBP)
elif tune.which() == 'torque':
- self.assertTrue(not math.isnan(tune.torque.kf) and tune.torque.kf > 0)
- self.assertTrue(not math.isnan(tune.torque.friction) and tune.torque.friction > 0)
+ assert not math.isnan(tune.torque.kf) and tune.torque.kf > 0
+ assert not math.isnan(tune.torque.friction) and tune.torque.friction > 0
cc_msg = FuzzyGenerator.get_random_msg(data.draw, car.CarControl, real_floats=True)
# Run car interface
@@ -96,16 +100,14 @@ class TestCarInterfaces(unittest.TestCase):
CC = car.CarControl.new_message(**cc_msg)
for _ in range(10):
car_interface.update(CC, [])
- car_interface.apply(CC, now_nanos)
- car_interface.apply(CC, now_nanos)
+ car_interface.apply(CC.as_reader(), now_nanos)
now_nanos += DT_CTRL * 1e9 # 10 ms
CC = car.CarControl.new_message(**cc_msg)
CC.enabled = True
for _ in range(10):
car_interface.update(CC, [])
- car_interface.apply(CC, now_nanos)
- car_interface.apply(CC, now_nanos)
+ car_interface.apply(CC.as_reader(), now_nanos)
now_nanos += DT_CTRL * 1e9 # 10ms
# Test controller initialization
@@ -134,33 +136,29 @@ class TestCarInterfaces(unittest.TestCase):
if not car_params.radarUnavailable and radar_interface.rcp is not None:
cans = [messaging.new_message('can', 1).to_bytes() for _ in range(5)]
rr = radar_interface.update(cans)
- self.assertTrue(rr is None or len(rr.errors) > 0)
+ assert rr is None or len(rr.errors) > 0
def test_interface_attrs(self):
"""Asserts basic behavior of interface attribute getter"""
num_brands = len(get_interface_attr('CAR'))
- self.assertGreaterEqual(num_brands, 13)
+ assert num_brands >= 13
# Should return value for all brands when not combining, even if attribute doesn't exist
ret = get_interface_attr('FAKE_ATTR')
- self.assertEqual(len(ret), num_brands)
+ assert len(ret) == num_brands
# Make sure we can combine dicts
ret = get_interface_attr('DBC', combine_brands=True)
- self.assertGreaterEqual(len(ret), 160)
+ assert len(ret) >= 160
# We don't support combining non-dicts
ret = get_interface_attr('CAR', combine_brands=True)
- self.assertEqual(len(ret), 0)
+ assert len(ret) == 0
# If brand has None value, it shouldn't return when ignore_none=True is specified
none_brands = {b for b, v in get_interface_attr('FINGERPRINTS').items() if v is None}
- self.assertGreaterEqual(len(none_brands), 1)
+ assert len(none_brands) >= 1
ret = get_interface_attr('FINGERPRINTS', ignore_none=True)
none_brands_in_ret = none_brands.intersection(ret)
- self.assertEqual(len(none_brands_in_ret), 0, f'Brands with None values in ignore_none=True result: {none_brands_in_ret}')
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert len(none_brands_in_ret) == 0, f'Brands with None values in ignore_none=True result: {none_brands_in_ret}'
diff --git a/selfdrive/car/tests/test_docs.py b/selfdrive/car/tests/test_docs.py
index 143b402d5f..0ed95e18f2 100755
--- a/selfdrive/car/tests/test_docs.py
+++ b/selfdrive/car/tests/test_docs.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
from collections import defaultdict
import os
+import pytest
import re
-import unittest
from openpilot.common.basedir import BASEDIR
from openpilot.selfdrive.car.car_helpers import interfaces
@@ -14,9 +14,9 @@ from openpilot.selfdrive.debug.dump_car_docs import dump_car_docs
from openpilot.selfdrive.debug.print_docs_diff import print_car_docs_diff
-class TestCarDocs(unittest.TestCase):
+class TestCarDocs:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
cls.all_cars = get_all_car_docs()
def test_generator(self):
@@ -24,8 +24,7 @@ class TestCarDocs(unittest.TestCase):
with open(CARS_MD_OUT) as f:
current_cars_md = f.read()
- self.assertEqual(generated_cars_md, current_cars_md,
- "Run selfdrive/car/docs.py to update the compatibility documentation")
+ assert generated_cars_md == current_cars_md, "Run selfdrive/car/docs.py to update the compatibility documentation"
def test_docs_diff(self):
dump_path = os.path.join(BASEDIR, "selfdrive", "car", "tests", "cars_dump")
@@ -33,65 +32,61 @@ class TestCarDocs(unittest.TestCase):
print_car_docs_diff(dump_path)
os.remove(dump_path)
- def test_duplicate_years(self):
+ def test_duplicate_years(self, subtests):
make_model_years = defaultdict(list)
for car in self.all_cars:
- with self.subTest(car_docs_name=car.name):
+ with subtests.test(car_docs_name=car.name):
make_model = (car.make, car.model)
for year in car.year_list:
- self.assertNotIn(year, make_model_years[make_model], f"{car.name}: Duplicate model year")
+ assert year not in make_model_years[make_model], f"{car.name}: Duplicate model year"
make_model_years[make_model].append(year)
- def test_missing_car_docs(self):
+ def test_missing_car_docs(self, subtests):
all_car_docs_platforms = [name for name, config in PLATFORMS.items()]
for platform in sorted(interfaces.keys()):
- with self.subTest(platform=platform):
- self.assertTrue(platform in all_car_docs_platforms, f"Platform: {platform} doesn't have a CarDocs entry")
+ with subtests.test(platform=platform):
+ assert platform in all_car_docs_platforms, f"Platform: {platform} doesn't have a CarDocs entry"
- def test_naming_conventions(self):
+ def test_naming_conventions(self, subtests):
# Asserts market-standard car naming conventions by brand
for car in self.all_cars:
- with self.subTest(car=car):
+ with subtests.test(car=car.name):
tokens = car.model.lower().split(" ")
if car.car_name == "hyundai":
- self.assertNotIn("phev", tokens, "Use `Plug-in Hybrid`")
- self.assertNotIn("hev", tokens, "Use `Hybrid`")
+ assert "phev" not in tokens, "Use `Plug-in Hybrid`"
+ assert "hev" not in tokens, "Use `Hybrid`"
if "plug-in hybrid" in car.model.lower():
- self.assertIn("Plug-in Hybrid", car.model, "Use correct capitalization")
+ assert "Plug-in Hybrid" in car.model, "Use correct capitalization"
if car.make != "Kia":
- self.assertNotIn("ev", tokens, "Use `Electric`")
+ assert "ev" not in tokens, "Use `Electric`"
elif car.car_name == "toyota":
if "rav4" in tokens:
- self.assertIn("RAV4", car.model, "Use correct capitalization")
+ assert "RAV4" in car.model, "Use correct capitalization"
- def test_torque_star(self):
+ def test_torque_star(self, subtests):
# Asserts brand-specific assumptions around steering torque star
for car in self.all_cars:
- with self.subTest(car=car):
+ with subtests.test(car=car.name):
# honda sanity check, it's the definition of a no torque star
if car.car_fingerprint in (HONDA.HONDA_ACCORD, HONDA.HONDA_CIVIC, HONDA.HONDA_CRV, HONDA.HONDA_ODYSSEY, HONDA.HONDA_PILOT):
- self.assertEqual(car.row[Column.STEERING_TORQUE], Star.EMPTY, f"{car.name} has full torque star")
+ assert car.row[Column.STEERING_TORQUE] == Star.EMPTY, f"{car.name} has full torque star"
elif car.car_name in ("toyota", "hyundai"):
- self.assertNotEqual(car.row[Column.STEERING_TORQUE], Star.EMPTY, f"{car.name} has no torque star")
+ assert car.row[Column.STEERING_TORQUE] != Star.EMPTY, f"{car.name} has no torque star"
- def test_year_format(self):
+ def test_year_format(self, subtests):
for car in self.all_cars:
- with self.subTest(car=car):
- self.assertIsNone(re.search(r"\d{4}-\d{4}", car.name), f"Format years correctly: {car.name}")
+ with subtests.test(car=car.name):
+ assert re.search(r"\d{4}-\d{4}", car.name) is None, f"Format years correctly: {car.name}"
- def test_harnesses(self):
+ def test_harnesses(self, subtests):
for car in self.all_cars:
- with self.subTest(car=car):
+ with subtests.test(car=car.name):
if car.name == "comma body":
- raise unittest.SkipTest
+ pytest.skip()
car_part_type = [p.part_type for p in car.car_parts.all_parts()]
car_parts = list(car.car_parts.all_parts())
- self.assertTrue(len(car_parts) > 0, f"Need to specify car parts: {car.name}")
- self.assertTrue(car_part_type.count(PartType.connector) == 1, f"Need to specify one harness connector: {car.name}")
- self.assertTrue(car_part_type.count(PartType.mount) == 1, f"Need to specify one mount: {car.name}")
- self.assertTrue(Cable.right_angle_obd_c_cable_1_5ft in car_parts, f"Need to specify a right angle OBD-C cable (1.5ft): {car.name}")
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert len(car_parts) > 0, f"Need to specify car parts: {car.name}"
+ assert car_part_type.count(PartType.connector) == 1, f"Need to specify one harness connector: {car.name}"
+ assert car_part_type.count(PartType.mount) == 1, f"Need to specify one mount: {car.name}"
+ assert Cable.right_angle_obd_c_cable_1_5ft in car_parts, f"Need to specify a right angle OBD-C cable (1.5ft): {car.name}"
diff --git a/selfdrive/car/tests/test_fw_fingerprint.py b/selfdrive/car/tests/test_fw_fingerprint.py
index ed5edbef31..230e6f10e1 100755
--- a/selfdrive/car/tests/test_fw_fingerprint.py
+++ b/selfdrive/car/tests/test_fw_fingerprint.py
@@ -1,10 +1,9 @@
#!/usr/bin/env python3
+import pytest
import random
import time
-import unittest
from collections import defaultdict
from parameterized import parameterized
-from unittest import mock
from cereal import car
from openpilot.selfdrive.car.car_helpers import interfaces
@@ -27,11 +26,11 @@ class FakeSocket:
pass
-class TestFwFingerprint(unittest.TestCase):
+class TestFwFingerprint:
def assertFingerprints(self, candidates, expected):
candidates = list(candidates)
- self.assertEqual(len(candidates), 1, f"got more than one candidate: {candidates}")
- self.assertEqual(candidates[0], expected)
+ assert len(candidates) == 1, f"got more than one candidate: {candidates}"
+ assert candidates[0] == expected
@parameterized.expand([(b, c, e[c], n) for b, e in VERSIONS.items() for c in e for n in (True, False)])
def test_exact_match(self, brand, car_model, ecus, test_non_essential):
@@ -62,7 +61,7 @@ class TestFwFingerprint(unittest.TestCase):
# Assert brand-specific fuzzy fingerprinting function doesn't disagree with standard fuzzy function
config = FW_QUERY_CONFIGS[brand]
if config.match_fw_to_car_fuzzy is None:
- raise unittest.SkipTest("Brand does not implement custom fuzzy fingerprinting function")
+ pytest.skip("Brand does not implement custom fuzzy fingerprinting function")
CP = car.CarParams.new_message()
for _ in range(5):
@@ -77,14 +76,14 @@ class TestFwFingerprint(unittest.TestCase):
# If both have matches, they must agree
if len(matches) == 1 and len(brand_matches) == 1:
- self.assertEqual(matches, brand_matches)
+ assert matches == brand_matches
@parameterized.expand([(b, c, e[c]) for b, e in VERSIONS.items() for c in e])
def test_fuzzy_match_ecu_count(self, brand, car_model, ecus):
# Asserts that fuzzy matching does not count matching FW, but ECU address keys
valid_ecus = [e for e in ecus if e[0] not in FUZZY_EXCLUDE_ECUS]
if not len(valid_ecus):
- raise unittest.SkipTest("Car model has no compatible ECUs for fuzzy matching")
+ pytest.skip("Car model has no compatible ECUs for fuzzy matching")
fw = []
for ecu in valid_ecus:
@@ -99,19 +98,19 @@ class TestFwFingerprint(unittest.TestCase):
# Assert no match if there are not enough unique ECUs
unique_ecus = {(f['address'], f['subAddress']) for f in fw}
if len(unique_ecus) < 2:
- self.assertEqual(len(matches), 0, car_model)
+ assert len(matches) == 0, car_model
# There won't always be a match due to shared FW, but if there is it should be correct
elif len(matches):
self.assertFingerprints(matches, car_model)
- def test_fw_version_lists(self):
+ def test_fw_version_lists(self, subtests):
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
for ecu, ecu_fw in ecus.items():
- with self.subTest(ecu):
+ with subtests.test(ecu):
duplicates = {fw for fw in ecu_fw if ecu_fw.count(fw) > 1}
- self.assertFalse(len(duplicates), f'{car_model}: Duplicate FW versions: Ecu.{ECU_NAME[ecu[0]]}, {duplicates}')
- self.assertGreater(len(ecu_fw), 0, f'{car_model}: No FW versions: Ecu.{ECU_NAME[ecu[0]]}')
+ assert not len(duplicates), f'{car_model}: Duplicate FW versions: Ecu.{ECU_NAME[ecu[0]]}, {duplicates}'
+ assert len(ecu_fw) > 0, f'{car_model}: No FW versions: Ecu.{ECU_NAME[ecu[0]]}'
def test_all_addrs_map_to_one_ecu(self):
for brand, cars in VERSIONS.items():
@@ -121,59 +120,59 @@ class TestFwFingerprint(unittest.TestCase):
addr_to_ecu[(addr, sub_addr)].add(ecu_type)
ecus_for_addr = addr_to_ecu[(addr, sub_addr)]
ecu_strings = ", ".join([f'Ecu.{ECU_NAME[ecu]}' for ecu in ecus_for_addr])
- self.assertLessEqual(len(ecus_for_addr), 1, f"{brand} has multiple ECUs that map to one address: {ecu_strings} -> ({hex(addr)}, {sub_addr})")
+ assert len(ecus_for_addr) <= 1, f"{brand} has multiple ECUs that map to one address: {ecu_strings} -> ({hex(addr)}, {sub_addr})"
- def test_data_collection_ecus(self):
+ def test_data_collection_ecus(self, subtests):
# Asserts no extra ECUs are in the fingerprinting database
for brand, config in FW_QUERY_CONFIGS.items():
for car_model, ecus in VERSIONS[brand].items():
bad_ecus = set(ecus).intersection(config.extra_ecus)
- with self.subTest(car_model=car_model.value):
- self.assertFalse(len(bad_ecus), f'{car_model}: Fingerprints contain ECUs added for data collection: {bad_ecus}')
+ with subtests.test(car_model=car_model.value):
+ assert not len(bad_ecus), f'{car_model}: Fingerprints contain ECUs added for data collection: {bad_ecus}'
- def test_blacklisted_ecus(self):
+ def test_blacklisted_ecus(self, subtests):
blacklisted_addrs = (0x7c4, 0x7d0) # includes A/C ecu and an unknown ecu
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
CP = interfaces[car_model][0].get_non_essential_params(car_model)
if CP.carName == 'subaru':
for ecu in ecus.keys():
- self.assertNotIn(ecu[1], blacklisted_addrs, f'{car_model}: Blacklisted ecu: (Ecu.{ECU_NAME[ecu[0]]}, {hex(ecu[1])})')
+ assert ecu[1] not in blacklisted_addrs, f'{car_model}: Blacklisted ecu: (Ecu.{ECU_NAME[ecu[0]]}, {hex(ecu[1])})'
elif CP.carName == "chrysler":
# Some HD trucks have a combined TCM and ECM
if CP.carFingerprint.startswith("RAM HD"):
for ecu in ecus.keys():
- self.assertNotEqual(ecu[0], Ecu.transmission, f"{car_model}: Blacklisted ecu: (Ecu.{ECU_NAME[ecu[0]]}, {hex(ecu[1])})")
+ assert ecu[0] != Ecu.transmission, f"{car_model}: Blacklisted ecu: (Ecu.{ECU_NAME[ecu[0]]}, {hex(ecu[1])})"
- def test_non_essential_ecus(self):
+ def test_non_essential_ecus(self, subtests):
for brand, config in FW_QUERY_CONFIGS.items():
- with self.subTest(brand):
+ with subtests.test(brand):
# These ECUs are already not in ESSENTIAL_ECUS which the fingerprint functions give a pass if missing
unnecessary_non_essential_ecus = set(config.non_essential_ecus) - set(ESSENTIAL_ECUS)
- self.assertEqual(unnecessary_non_essential_ecus, set(), "Declaring non-essential ECUs non-essential is not required: " +
- f"{', '.join([f'Ecu.{ECU_NAME[ecu]}' for ecu in unnecessary_non_essential_ecus])}")
+ assert unnecessary_non_essential_ecus == set(), "Declaring non-essential ECUs non-essential is not required: " + \
+ f"{', '.join([f'Ecu.{ECU_NAME[ecu]}' for ecu in unnecessary_non_essential_ecus])}"
- def test_missing_versions_and_configs(self):
+ def test_missing_versions_and_configs(self, subtests):
brand_versions = set(VERSIONS.keys())
brand_configs = set(FW_QUERY_CONFIGS.keys())
if len(brand_configs - brand_versions):
- with self.subTest():
- self.fail(f"Brands do not implement FW_VERSIONS: {brand_configs - brand_versions}")
+ with subtests.test():
+ pytest.fail(f"Brands do not implement FW_VERSIONS: {brand_configs - brand_versions}")
if len(brand_versions - brand_configs):
- with self.subTest():
- self.fail(f"Brands do not implement FW_QUERY_CONFIG: {brand_versions - brand_configs}")
+ with subtests.test():
+ pytest.fail(f"Brands do not implement FW_QUERY_CONFIG: {brand_versions - brand_configs}")
# Ensure each brand has at least 1 ECU to query, and extra ECU retrieval
for brand, config in FW_QUERY_CONFIGS.items():
- self.assertEqual(len(config.get_all_ecus({}, include_extra_ecus=False)), 0)
- self.assertEqual(config.get_all_ecus({}), set(config.extra_ecus))
- self.assertGreater(len(config.get_all_ecus(VERSIONS[brand])), 0)
+ assert len(config.get_all_ecus({}, include_extra_ecus=False)) == 0
+ assert config.get_all_ecus({}) == set(config.extra_ecus)
+ assert len(config.get_all_ecus(VERSIONS[brand])) > 0
- def test_fw_request_ecu_whitelist(self):
+ def test_fw_request_ecu_whitelist(self, subtests):
for brand, config in FW_QUERY_CONFIGS.items():
- with self.subTest(brand=brand):
+ with subtests.test(brand=brand):
whitelisted_ecus = {ecu for r in config.requests for ecu in r.whitelist_ecus}
brand_ecus = {fw[0] for car_fw in VERSIONS[brand].values() for fw in car_fw}
brand_ecus |= {ecu[0] for ecu in config.extra_ecus}
@@ -182,30 +181,30 @@ class TestFwFingerprint(unittest.TestCase):
ecus_not_whitelisted = brand_ecus - whitelisted_ecus
ecu_strings = ", ".join([f'Ecu.{ECU_NAME[ecu]}' for ecu in ecus_not_whitelisted])
- self.assertFalse(len(whitelisted_ecus) and len(ecus_not_whitelisted),
- f'{brand.title()}: ECUs not in any FW query whitelists: {ecu_strings}')
+ assert not (len(whitelisted_ecus) and len(ecus_not_whitelisted)), \
+ f'{brand.title()}: ECUs not in any FW query whitelists: {ecu_strings}'
- def test_fw_requests(self):
+ def test_fw_requests(self, subtests):
# Asserts equal length request and response lists
for brand, config in FW_QUERY_CONFIGS.items():
- with self.subTest(brand=brand):
+ with subtests.test(brand=brand):
for request_obj in config.requests:
- self.assertEqual(len(request_obj.request), len(request_obj.response))
+ assert len(request_obj.request) == len(request_obj.response)
# No request on the OBD port (bus 1, multiplexed) should be run on an aux panda
- self.assertFalse(request_obj.auxiliary and request_obj.bus == 1 and request_obj.obd_multiplexing,
- f"{brand.title()}: OBD multiplexed request is marked auxiliary: {request_obj}")
+ assert not (request_obj.auxiliary and request_obj.bus == 1 and request_obj.obd_multiplexing), \
+ f"{brand.title()}: OBD multiplexed request is marked auxiliary: {request_obj}"
def test_brand_ecu_matches(self):
empty_response = {brand: set() for brand in FW_QUERY_CONFIGS}
- self.assertEqual(get_brand_ecu_matches(set()), empty_response)
+ assert get_brand_ecu_matches(set()) == empty_response
# we ignore bus
expected_response = empty_response | {'toyota': {(0x750, 0xf)}}
- self.assertEqual(get_brand_ecu_matches({(0x758, 0xf, 99)}), expected_response)
+ assert get_brand_ecu_matches({(0x758, 0xf, 99)}) == expected_response
-class TestFwFingerprintTiming(unittest.TestCase):
+class TestFwFingerprintTiming:
N: int = 5
TOL: float = 0.05
@@ -223,26 +222,26 @@ class TestFwFingerprintTiming(unittest.TestCase):
self.total_time += timeout
return {}
- def _benchmark_brand(self, brand, num_pandas):
+ def _benchmark_brand(self, brand, num_pandas, mocker):
fake_socket = FakeSocket()
self.total_time = 0
- with (mock.patch("openpilot.selfdrive.car.fw_versions.set_obd_multiplexing", self.fake_set_obd_multiplexing),
- mock.patch("openpilot.selfdrive.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data)):
- for _ in range(self.N):
- # Treat each brand as the most likely (aka, the first) brand with OBD multiplexing initially on
- self.current_obd_multiplexing = True
+ mocker.patch("openpilot.selfdrive.car.fw_versions.set_obd_multiplexing", self.fake_set_obd_multiplexing)
+ mocker.patch("openpilot.selfdrive.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data)
+ for _ in range(self.N):
+ # Treat each brand as the most likely (aka, the first) brand with OBD multiplexing initially on
+ self.current_obd_multiplexing = True
- t = time.perf_counter()
- get_fw_versions(fake_socket, fake_socket, brand, num_pandas=num_pandas)
- self.total_time += time.perf_counter() - t
+ t = time.perf_counter()
+ get_fw_versions(fake_socket, fake_socket, brand, num_pandas=num_pandas)
+ self.total_time += time.perf_counter() - t
return self.total_time / self.N
def _assert_timing(self, avg_time, ref_time):
- self.assertLess(avg_time, ref_time + self.TOL)
- self.assertGreater(avg_time, ref_time - self.TOL, "Performance seems to have improved, update test refs.")
+ assert avg_time < ref_time + self.TOL
+ assert avg_time > ref_time - self.TOL, "Performance seems to have improved, update test refs."
- def test_startup_timing(self):
+ def test_startup_timing(self, subtests, mocker):
# Tests worse-case VIN query time and typical present ECU query time
vin_ref_times = {'worst': 1.4, 'best': 0.7} # best assumes we go through all queries to get a match
present_ecu_ref_time = 0.45
@@ -253,24 +252,24 @@ class TestFwFingerprintTiming(unittest.TestCase):
fake_socket = FakeSocket()
self.total_time = 0.0
- with (mock.patch("openpilot.selfdrive.car.fw_versions.set_obd_multiplexing", self.fake_set_obd_multiplexing),
- mock.patch("openpilot.selfdrive.car.fw_versions.get_ecu_addrs", fake_get_ecu_addrs)):
- for _ in range(self.N):
- self.current_obd_multiplexing = True
- get_present_ecus(fake_socket, fake_socket, num_pandas=2)
+ mocker.patch("openpilot.selfdrive.car.fw_versions.set_obd_multiplexing", self.fake_set_obd_multiplexing)
+ mocker.patch("openpilot.selfdrive.car.fw_versions.get_ecu_addrs", fake_get_ecu_addrs)
+ for _ in range(self.N):
+ self.current_obd_multiplexing = True
+ get_present_ecus(fake_socket, fake_socket, num_pandas=2)
self._assert_timing(self.total_time / self.N, present_ecu_ref_time)
print(f'get_present_ecus, query time={self.total_time / self.N} seconds')
for name, args in (('worst', {}), ('best', {'retry': 1})):
- with self.subTest(name=name):
+ with subtests.test(name=name):
self.total_time = 0.0
- with (mock.patch("openpilot.selfdrive.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data)):
- for _ in range(self.N):
- get_vin(fake_socket, fake_socket, (0, 1), **args)
+ mocker.patch("openpilot.selfdrive.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data)
+ for _ in range(self.N):
+ get_vin(fake_socket, fake_socket, (0, 1), **args)
self._assert_timing(self.total_time / self.N, vin_ref_times[name])
print(f'get_vin {name} case, query time={self.total_time / self.N} seconds')
- def test_fw_query_timing(self):
+ def test_fw_query_timing(self, subtests, mocker):
total_ref_time = {1: 7.2, 2: 7.8}
brand_ref_times = {
1: {
@@ -297,8 +296,8 @@ class TestFwFingerprintTiming(unittest.TestCase):
total_times = {1: 0.0, 2: 0.0}
for num_pandas in (1, 2):
for brand, config in FW_QUERY_CONFIGS.items():
- with self.subTest(brand=brand, num_pandas=num_pandas):
- avg_time = self._benchmark_brand(brand, num_pandas)
+ with subtests.test(brand=brand, num_pandas=num_pandas):
+ avg_time = self._benchmark_brand(brand, num_pandas, mocker)
total_times[num_pandas] += avg_time
avg_time = round(avg_time, 2)
@@ -311,11 +310,7 @@ class TestFwFingerprintTiming(unittest.TestCase):
print(f'{brand=}, {num_pandas=}, {len(config.requests)=}, avg FW query time={avg_time} seconds')
for num_pandas in (1, 2):
- with self.subTest(brand='all_brands', num_pandas=num_pandas):
+ with subtests.test(brand='all_brands', num_pandas=num_pandas):
total_time = round(total_times[num_pandas], 2)
self._assert_timing(total_time, total_ref_time[num_pandas])
print(f'all brands, total FW query time={total_time} seconds')
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/car/tests/test_lateral_limits.py b/selfdrive/car/tests/test_lateral_limits.py
index e5cfd972bd..a478bc601a 100755
--- a/selfdrive/car/tests/test_lateral_limits.py
+++ b/selfdrive/car/tests/test_lateral_limits.py
@@ -2,8 +2,7 @@
from collections import defaultdict
import importlib
from parameterized import parameterized_class
-import sys
-import unittest
+import pytest
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.car.car_helpers import interfaces
@@ -25,23 +24,23 @@ car_model_jerks: defaultdict[str, dict[str, float]] = defaultdict(dict)
@parameterized_class('car_model', [(c,) for c in sorted(CAR_MODELS)])
-class TestLateralLimits(unittest.TestCase):
+class TestLateralLimits:
car_model: str
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
CarInterface, _, _ = interfaces[cls.car_model]
CP = CarInterface.get_non_essential_params(cls.car_model)
if CP.dashcamOnly:
- raise unittest.SkipTest("Platform is behind dashcamOnly")
+ pytest.skip("Platform is behind dashcamOnly")
# TODO: test all platforms
if CP.lateralTuning.which() != 'torque':
- raise unittest.SkipTest
+ pytest.skip()
if CP.notCar:
- raise unittest.SkipTest
+ pytest.skip()
CarControllerParams = importlib.import_module(f'selfdrive.car.{CP.carName}.values').CarControllerParams
cls.control_params = CarControllerParams(CP)
@@ -66,26 +65,8 @@ class TestLateralLimits(unittest.TestCase):
def test_jerk_limits(self):
up_jerk, down_jerk = self.calculate_0_5s_jerk(self.control_params, self.torque_params)
car_model_jerks[self.car_model] = {"up_jerk": up_jerk, "down_jerk": down_jerk}
- self.assertLessEqual(up_jerk, MAX_LAT_JERK_UP + MAX_LAT_JERK_UP_TOLERANCE)
- self.assertLessEqual(down_jerk, MAX_LAT_JERK_DOWN)
+ assert up_jerk <= MAX_LAT_JERK_UP + MAX_LAT_JERK_UP_TOLERANCE
+ assert down_jerk <= MAX_LAT_JERK_DOWN
def test_max_lateral_accel(self):
- self.assertLessEqual(self.torque_params["MAX_LAT_ACCEL_MEASURED"], MAX_LAT_ACCEL)
-
-
-if __name__ == "__main__":
- result = unittest.main(exit=False)
-
- print(f"\n\n---- Lateral limit report ({len(CAR_MODELS)} cars) ----\n")
-
- max_car_model_len = max([len(car_model) for car_model in car_model_jerks])
- for car_model, _jerks in sorted(car_model_jerks.items(), key=lambda i: i[1]['up_jerk'], reverse=True):
- violation = _jerks["up_jerk"] > MAX_LAT_JERK_UP + MAX_LAT_JERK_UP_TOLERANCE or \
- _jerks["down_jerk"] > MAX_LAT_JERK_DOWN
- violation_str = " - VIOLATION" if violation else ""
-
- print(f"{car_model:{max_car_model_len}} - up jerk: {round(_jerks['up_jerk'], 2):5} " +
- f"m/s^3, down jerk: {round(_jerks['down_jerk'], 2):5} m/s^3{violation_str}")
-
- # exit with test result
- sys.exit(not result.result.wasSuccessful())
+ assert self.torque_params["MAX_LAT_ACCEL_MEASURED"] <= MAX_LAT_ACCEL
diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py
index 2a0626c590..8807db69d9 100755
--- a/selfdrive/car/tests/test_models.py
+++ b/selfdrive/car/tests/test_models.py
@@ -4,7 +4,7 @@ import os
import importlib
import pytest
import random
-import unittest
+import unittest # noqa: TID251
from collections import defaultdict, Counter
import hypothesis.strategies as st
from hypothesis import Phase, given, settings
@@ -15,12 +15,12 @@ from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.car import gen_empty_fingerprint
+from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.car.fingerprints import all_known_cars, MIGRATION
from openpilot.selfdrive.car.car_helpers import FRAME_FINGERPRINT, interfaces
from openpilot.selfdrive.car.honda.values import CAR as HONDA, HondaFlags
from openpilot.selfdrive.car.tests.routes import non_tested_cars, routes, CarTestRoute
from openpilot.selfdrive.car.values import Platform
-from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.selfdrive.test.helpers import read_segment_list
from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
from openpilot.tools.lib.logreader import LogReader, internal_source, openpilotci_source
@@ -215,7 +215,7 @@ class TestCarModelBase(unittest.TestCase):
# TODO: also check for checksum violations from can parser
can_invalid_cnt = 0
can_valid = False
- CC = car.CarControl.new_message()
+ CC = car.CarControl.new_message().as_reader()
for i, msg in enumerate(self.can_msgs):
CS = self.CI.update(CC, (msg.as_builder().to_bytes(),))
@@ -308,17 +308,17 @@ class TestCarModelBase(unittest.TestCase):
# Make sure we can send all messages while inactive
CC = car.CarControl.new_message()
- test_car_controller(CC)
+ test_car_controller(CC.as_reader())
# Test cancel + general messages (controls_allowed=False & cruise_engaged=True)
self.safety.set_cruise_engaged_prev(True)
CC = car.CarControl.new_message(cruiseControl={'cancel': True})
- test_car_controller(CC)
+ test_car_controller(CC.as_reader())
# Test resume + general messages (controls_allowed=True & cruise_engaged=True)
self.safety.set_controls_allowed(True)
CC = car.CarControl.new_message(cruiseControl={'resume': True})
- test_car_controller(CC)
+ test_car_controller(CC.as_reader())
# Skip stdout/stderr capture with pytest, causes elevated memory usage
@pytest.mark.nocapture
@@ -406,8 +406,7 @@ class TestCarModelBase(unittest.TestCase):
controls_allowed_prev = False
CS_prev = car.CarState.new_message()
checks = defaultdict(int)
- controlsd = Controls(CI=self.CI)
- controlsd.initialized = True
+ card = Car(CI=self.CI)
for idx, can in enumerate(self.can_msgs):
CS = self.CI.update(CC, (can.as_builder().to_bytes(), ))
for msg in filter(lambda m: m.src in range(64), can.can):
@@ -452,10 +451,10 @@ class TestCarModelBase(unittest.TestCase):
checks['cruiseState'] += CS.cruiseState.enabled != self.safety.get_cruise_engaged_prev()
else:
# Check for enable events on rising edge of controls allowed
- controlsd.update_events(CS)
- controlsd.CS_prev = CS
+ card.update_events(CS)
+ card.CS_prev = CS
button_enable = (any(evt.enable for evt in CS.events) and
- not any(evt == EventName.pedalPressed for evt in controlsd.events.names))
+ not any(evt == EventName.pedalPressed for evt in card.events.names))
mismatch = button_enable != (self.safety.get_controls_allowed() and not controls_allowed_prev)
checks['controlsAllowed'] += mismatch
controls_allowed_prev = self.safety.get_controls_allowed()
diff --git a/selfdrive/car/tests/test_platform_configs.py b/selfdrive/car/tests/test_platform_configs.py
index 523c331b9e..217189255e 100755
--- a/selfdrive/car/tests/test_platform_configs.py
+++ b/selfdrive/car/tests/test_platform_configs.py
@@ -1,25 +1,19 @@
#!/usr/bin/env python3
-import unittest
-
from openpilot.selfdrive.car.values import PLATFORMS
-class TestPlatformConfigs(unittest.TestCase):
- def test_configs(self):
+class TestPlatformConfigs:
+ def test_configs(self, subtests):
for name, platform in PLATFORMS.items():
- with self.subTest(platform=str(platform)):
- self.assertTrue(platform.config._frozen)
+ with subtests.test(platform=str(platform)):
+ assert platform.config._frozen
if platform != "MOCK":
- self.assertIn("pt", platform.config.dbc_dict)
- self.assertTrue(len(platform.config.platform_str) > 0)
+ assert "pt" in platform.config.dbc_dict
+ assert len(platform.config.platform_str) > 0
- self.assertEqual(name, platform.config.platform_str)
+ assert name == platform.config.platform_str
- self.assertIsNotNone(platform.config.specs)
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert platform.config.specs is not None
diff --git a/selfdrive/car/toyota/carcontroller.py b/selfdrive/car/toyota/carcontroller.py
index 8d5629992a..b5ec48df13 100644
--- a/selfdrive/car/toyota/carcontroller.py
+++ b/selfdrive/car/toyota/carcontroller.py
@@ -122,11 +122,6 @@ class CarController(CarControllerBase):
interceptor_gas_cmd = 0.
pcm_accel_cmd = clip(actuators.accel, self.params.ACCEL_MIN, self.params.ACCEL_MAX)
- # TODO: probably can delete this. CS.pcm_acc_status uses a different signal
- # than CS.cruiseState.enabled. confirm they're not meaningfully different
- if not CC.enabled and CS.pcm_acc_status:
- pcm_cancel_cmd = 1
-
# on entering standstill, send standstill request
if CS.out.standstill and not self.last_standstill and (self.CP.carFingerprint not in NO_STOP_TIMER_CAR or self.CP.enableGasInterceptorDEPRECATED):
self.standstill_req = True
@@ -200,7 +195,7 @@ class CarController(CarControllerBase):
if self.frame % 20 == 0 and self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
can_sends.append([0x750, 0, b"\x0F\x02\x3E\x00\x00\x00\x00\x00", 0])
- new_actuators = actuators.copy()
+ new_actuators = actuators.as_builder()
new_actuators.steer = apply_steer / self.params.STEER_MAX
new_actuators.steerOutputCan = apply_steer
new_actuators.steeringAngleDeg = self.last_angle
diff --git a/selfdrive/car/toyota/fingerprints.py b/selfdrive/car/toyota/fingerprints.py
index a57e58fbb7..6319535714 100644
--- a/selfdrive/car/toyota/fingerprints.py
+++ b/selfdrive/car/toyota/fingerprints.py
@@ -521,6 +521,7 @@ FW_VERSIONS = {
],
(Ecu.abs, 0x7b0, None): [
b'\x01F152602280\x00\x00\x00\x00\x00\x00',
+ b'\x01F152602281\x00\x00\x00\x00\x00\x00',
b'\x01F152602470\x00\x00\x00\x00\x00\x00',
b'\x01F152602560\x00\x00\x00\x00\x00\x00',
b'\x01F152602590\x00\x00\x00\x00\x00\x00',
@@ -1141,6 +1142,7 @@ FW_VERSIONS = {
CAR.TOYOTA_RAV4_TSS2_2023: {
(Ecu.abs, 0x7b0, None): [
b'\x01F15260R450\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260R50000\x00\x00\x00\x00',
b'\x01F15260R51000\x00\x00\x00\x00',
b'\x01F15264283200\x00\x00\x00\x00',
b'\x01F15264283300\x00\x00\x00\x00',
@@ -1162,6 +1164,7 @@ FW_VERSIONS = {
b'\x01896634AJ2000\x00\x00\x00\x00',
b'\x01896634AL5000\x00\x00\x00\x00',
b'\x01896634AL6000\x00\x00\x00\x00',
+ b'\x01896634AL8000\x00\x00\x00\x00',
],
(Ecu.fwdRadar, 0x750, 0xf): [
b'\x018821F0R03100\x00\x00\x00\x00',
@@ -1474,6 +1477,7 @@ FW_VERSIONS = {
b'\x01896630E41200\x00\x00\x00\x00',
b'\x01896630E41500\x00\x00\x00\x00',
b'\x01896630EA3100\x00\x00\x00\x00',
+ b'\x01896630EA3300\x00\x00\x00\x00',
b'\x01896630EA3400\x00\x00\x00\x00',
b'\x01896630EA4100\x00\x00\x00\x00',
b'\x01896630EA4300\x00\x00\x00\x00',
diff --git a/selfdrive/car/toyota/tests/test_toyota.py b/selfdrive/car/toyota/tests/test_toyota.py
index e2a9b46eb4..ef49e00551 100755
--- a/selfdrive/car/toyota/tests/test_toyota.py
+++ b/selfdrive/car/toyota/tests/test_toyota.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
from hypothesis import given, settings, strategies as st
-import unittest
from cereal import car
from openpilot.selfdrive.car.fw_versions import build_fw_dict
@@ -17,59 +16,58 @@ def check_fw_version(fw_version: bytes) -> bool:
return b'?' not in fw_version
-class TestToyotaInterfaces(unittest.TestCase):
+class TestToyotaInterfaces:
def test_car_sets(self):
- self.assertTrue(len(ANGLE_CONTROL_CAR - TSS2_CAR) == 0)
- self.assertTrue(len(RADAR_ACC_CAR - TSS2_CAR) == 0)
+ assert len(ANGLE_CONTROL_CAR - TSS2_CAR) == 0
+ assert len(RADAR_ACC_CAR - TSS2_CAR) == 0
def test_lta_platforms(self):
# At this time, only RAV4 2023 is expected to use LTA/angle control
- self.assertEqual(ANGLE_CONTROL_CAR, {CAR.TOYOTA_RAV4_TSS2_2023})
+ assert ANGLE_CONTROL_CAR == {CAR.TOYOTA_RAV4_TSS2_2023}
def test_tss2_dbc(self):
# We make some assumptions about TSS2 platforms,
# like looking up certain signals only in this DBC
for car_model, dbc in DBC.items():
if car_model in TSS2_CAR:
- self.assertEqual(dbc["pt"], "toyota_nodsu_pt_generated")
+ assert dbc["pt"] == "toyota_nodsu_pt_generated"
- def test_essential_ecus(self):
+ def test_essential_ecus(self, subtests):
# Asserts standard ECUs exist for each platform
common_ecus = {Ecu.fwdRadar, Ecu.fwdCamera}
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
present_ecus = {ecu[0] for ecu in ecus}
missing_ecus = common_ecus - present_ecus
- self.assertEqual(len(missing_ecus), 0)
+ assert len(missing_ecus) == 0
# Some exceptions for other common ECUs
if car_model not in (CAR.TOYOTA_ALPHARD_TSS2,):
- self.assertIn(Ecu.abs, present_ecus)
+ assert Ecu.abs in present_ecus
if car_model not in (CAR.TOYOTA_MIRAI,):
- self.assertIn(Ecu.engine, present_ecus)
+ assert Ecu.engine in present_ecus
if car_model not in (CAR.TOYOTA_PRIUS_V, CAR.LEXUS_CTH):
- self.assertIn(Ecu.eps, present_ecus)
+ assert Ecu.eps in present_ecus
-class TestToyotaFingerprint(unittest.TestCase):
- def test_non_essential_ecus(self):
+class TestToyotaFingerprint:
+ def test_non_essential_ecus(self, subtests):
# Ensures only the cars that have multiple engine ECUs are in the engine non-essential ECU list
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
engine_ecus = {ecu for ecu in ecus if ecu[0] == Ecu.engine}
- self.assertEqual(len(engine_ecus) > 1,
- car_model in FW_QUERY_CONFIG.non_essential_ecus[Ecu.engine],
- f"Car model unexpectedly {'not ' if len(engine_ecus) > 1 else ''}in non-essential list")
+ assert (len(engine_ecus) > 1) == (car_model in FW_QUERY_CONFIG.non_essential_ecus[Ecu.engine]), \
+ f"Car model unexpectedly {'not ' if len(engine_ecus) > 1 else ''}in non-essential list"
- def test_valid_fw_versions(self):
+ def test_valid_fw_versions(self, subtests):
# Asserts all FW versions are valid
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
for fws in ecus.values():
for fw in fws:
- self.assertTrue(check_fw_version(fw), fw)
+ assert check_fw_version(fw), fw
# Tests for part numbers, platform codes, and sub-versions which Toyota will use to fuzzy
# fingerprint in the absence of full FW matches:
@@ -80,25 +78,25 @@ class TestToyotaFingerprint(unittest.TestCase):
fws = data.draw(fw_strategy)
get_platform_codes(fws)
- def test_platform_code_ecus_available(self):
+ def test_platform_code_ecus_available(self, subtests):
# Asserts ECU keys essential for fuzzy fingerprinting are available on all platforms
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
for platform_code_ecu in PLATFORM_CODE_ECUS:
if platform_code_ecu == Ecu.eps and car_model in (CAR.TOYOTA_PRIUS_V, CAR.LEXUS_CTH,):
continue
if platform_code_ecu == Ecu.abs and car_model in (CAR.TOYOTA_ALPHARD_TSS2,):
continue
- self.assertIn(platform_code_ecu, [e[0] for e in ecus])
+ assert platform_code_ecu in [e[0] for e in ecus]
- def test_fw_format(self):
+ def test_fw_format(self, subtests):
# Asserts:
# - every supported ECU FW version returns one platform code
# - every supported ECU FW version has a part number
# - expected parsing of ECU sub-versions
for car_model, ecus in FW_VERSIONS.items():
- with self.subTest(car_model=car_model.value):
+ with subtests.test(car_model=car_model.value):
for ecu, fws in ecus.items():
if ecu[0] not in PLATFORM_CODE_ECUS:
continue
@@ -107,15 +105,14 @@ class TestToyotaFingerprint(unittest.TestCase):
for fw in fws:
result = get_platform_codes([fw])
# Check only one platform code and sub-version
- self.assertEqual(1, len(result), f"Unable to parse FW: {fw}")
- self.assertEqual(1, len(list(result.values())[0]), f"Unable to parse FW: {fw}")
+ assert 1 == len(result), f"Unable to parse FW: {fw}"
+ assert 1 == len(list(result.values())[0]), f"Unable to parse FW: {fw}"
codes |= result
# Toyota places the ECU part number in their FW versions, assert all parsable
# Note that there is only one unique part number per ECU across the fleet, so this
# is not important for identification, just a sanity check.
- self.assertTrue(all(code.count(b"-") > 1 for code in codes),
- f"FW does not have part number: {fw} {codes}")
+ assert all(code.count(b"-") > 1 for code in codes), f"FW does not have part number: {fw} {codes}"
def test_platform_codes_spot_check(self):
# Asserts basic platform code parsing behavior for a few cases
@@ -125,20 +122,20 @@ class TestToyotaFingerprint(unittest.TestCase):
b"F152607110\x00\x00\x00\x00\x00\x00",
b"F152607180\x00\x00\x00\x00\x00\x00",
])
- self.assertEqual(results, {b"F1526-07-1": {b"10", b"40", b"71", b"80"}})
+ assert results == {b"F1526-07-1": {b"10", b"40", b"71", b"80"}}
results = get_platform_codes([
b"\x028646F4104100\x00\x00\x00\x008646G5301200\x00\x00\x00\x00",
b"\x028646F4104100\x00\x00\x00\x008646G3304000\x00\x00\x00\x00",
])
- self.assertEqual(results, {b"8646F-41-04": {b"100"}})
+ assert results == {b"8646F-41-04": {b"100"}}
# Short version has no part number
results = get_platform_codes([
b"\x0235870000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00",
b"\x0235883000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00",
])
- self.assertEqual(results, {b"58-70": {b"000"}, b"58-83": {b"000"}})
+ assert results == {b"58-70": {b"000"}, b"58-83": {b"000"}}
results = get_platform_codes([
b"F152607110\x00\x00\x00\x00\x00\x00",
@@ -146,7 +143,7 @@ class TestToyotaFingerprint(unittest.TestCase):
b"\x028646F4104100\x00\x00\x00\x008646G5301200\x00\x00\x00\x00",
b"\x0235879000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00",
])
- self.assertEqual(results, {b"F1526-07-1": {b"10", b"40"}, b"8646F-41-04": {b"100"}, b"58-79": {b"000"}})
+ assert results == {b"F1526-07-1": {b"10", b"40"}, b"8646F-41-04": {b"100"}, b"58-79": {b"000"}}
def test_fuzzy_excluded_platforms(self):
# Asserts a list of platforms that will not fuzzy fingerprint with platform codes due to them being shared.
@@ -162,13 +159,9 @@ class TestToyotaFingerprint(unittest.TestCase):
CP = car.CarParams.new_message(carFw=car_fw)
matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw), CP.carVin, FW_VERSIONS)
if len(matches) == 1:
- self.assertEqual(list(matches)[0], platform)
+ assert list(matches)[0] == platform
else:
# If a platform has multiple matches, add it and its matches
platforms_with_shared_codes |= {str(platform), *matches}
- self.assertEqual(platforms_with_shared_codes, FUZZY_EXCLUDED_PLATFORMS, (len(platforms_with_shared_codes), len(FW_VERSIONS)))
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert platforms_with_shared_codes == FUZZY_EXCLUDED_PLATFORMS, (len(platforms_with_shared_codes), len(FW_VERSIONS))
diff --git a/selfdrive/car/volkswagen/carcontroller.py b/selfdrive/car/volkswagen/carcontroller.py
index 5fc47c51c6..a4e0c8946a 100644
--- a/selfdrive/car/volkswagen/carcontroller.py
+++ b/selfdrive/car/volkswagen/carcontroller.py
@@ -112,7 +112,7 @@ class CarController(CarControllerBase):
can_sends.append(self.CCS.create_acc_buttons_control(self.packer_pt, self.ext_bus, CS.gra_stock_values,
cancel=CC.cruiseControl.cancel, resume=CC.cruiseControl.resume))
- new_actuators = actuators.copy()
+ new_actuators = actuators.as_builder()
new_actuators.steer = self.apply_steer_last / self.CCP.STEER_MAX
new_actuators.steerOutputCan = self.apply_steer_last
diff --git a/selfdrive/car/volkswagen/carstate.py b/selfdrive/car/volkswagen/carstate.py
index 4d9313a847..c2e41a344f 100644
--- a/selfdrive/car/volkswagen/carstate.py
+++ b/selfdrive/car/volkswagen/carstate.py
@@ -263,7 +263,7 @@ class CarState(CarStateBase):
# DISABLED means the EPS hasn't been configured to support Lane Assist
self.eps_init_complete = self.eps_init_complete or (hca_status in ("DISABLED", "READY", "ACTIVE") or self.frame > 600)
perm_fault = hca_status == "DISABLED" or (self.eps_init_complete and hca_status in ("INITIALIZING", "FAULT"))
- temp_fault = hca_status == "REJECTED" or not self.eps_init_complete
+ temp_fault = hca_status in ("REJECTED", "PREEMPTED") or not self.eps_init_complete
return temp_fault, perm_fault
@staticmethod
diff --git a/selfdrive/car/volkswagen/fingerprints.py b/selfdrive/car/volkswagen/fingerprints.py
index e20fa4fc1c..fdc375fc8d 100644
--- a/selfdrive/car/volkswagen/fingerprints.py
+++ b/selfdrive/car/volkswagen/fingerprints.py
@@ -228,6 +228,7 @@ FW_VERSIONS = {
b'\xf1\x870DD300046F \xf1\x891601',
b'\xf1\x870GC300012A \xf1\x891401',
b'\xf1\x870GC300012A \xf1\x891403',
+ b'\xf1\x870GC300012A \xf1\x891422',
b'\xf1\x870GC300012M \xf1\x892301',
b'\xf1\x870GC300014B \xf1\x892401',
b'\xf1\x870GC300014B \xf1\x892403',
diff --git a/selfdrive/car/volkswagen/tests/test_volkswagen.py b/selfdrive/car/volkswagen/tests/test_volkswagen.py
index 17331203bb..561d28b9fb 100755
--- a/selfdrive/car/volkswagen/tests/test_volkswagen.py
+++ b/selfdrive/car/volkswagen/tests/test_volkswagen.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import random
import re
-import unittest
from cereal import car
from openpilot.selfdrive.car.volkswagen.values import CAR, FW_QUERY_CONFIG, WMI
@@ -14,35 +13,35 @@ CHASSIS_CODE_PATTERN = re.compile('[A-Z0-9]{2}')
SPARE_PART_FW_PATTERN = re.compile(b'\xf1\x87(?P[0-9][0-9A-Z]{2})(?P[0-9][0-9A-Z][0-9])(?P[0-9A-Z]{2}[0-9])([A-Z0-9]| )')
-class TestVolkswagenPlatformConfigs(unittest.TestCase):
- def test_spare_part_fw_pattern(self):
+class TestVolkswagenPlatformConfigs:
+ def test_spare_part_fw_pattern(self, subtests):
# Relied on for determining if a FW is likely VW
for platform, ecus in FW_VERSIONS.items():
- with self.subTest(platform=platform):
+ with subtests.test(platform=platform.value):
for fws in ecus.values():
for fw in fws:
- self.assertNotEqual(SPARE_PART_FW_PATTERN.match(fw), None, f"Bad FW: {fw}")
+ assert SPARE_PART_FW_PATTERN.match(fw) is not None, f"Bad FW: {fw}"
- def test_chassis_codes(self):
+ def test_chassis_codes(self, subtests):
for platform in CAR:
- with self.subTest(platform=platform):
- self.assertTrue(len(platform.config.wmis) > 0, "WMIs not set")
- self.assertTrue(len(platform.config.chassis_codes) > 0, "Chassis codes not set")
- self.assertTrue(all(CHASSIS_CODE_PATTERN.match(cc) for cc in
- platform.config.chassis_codes), "Bad chassis codes")
+ with subtests.test(platform=platform.value):
+ assert len(platform.config.wmis) > 0, "WMIs not set"
+ assert len(platform.config.chassis_codes) > 0, "Chassis codes not set"
+ assert all(CHASSIS_CODE_PATTERN.match(cc) for cc in \
+ platform.config.chassis_codes), "Bad chassis codes"
# No two platforms should share chassis codes
for comp in CAR:
if platform == comp:
continue
- self.assertEqual(set(), platform.config.chassis_codes & comp.config.chassis_codes,
- f"Shared chassis codes: {comp}")
+ assert set() == platform.config.chassis_codes & comp.config.chassis_codes, \
+ f"Shared chassis codes: {comp}"
- def test_custom_fuzzy_fingerprinting(self):
+ def test_custom_fuzzy_fingerprinting(self, subtests):
all_radar_fw = list({fw for ecus in FW_VERSIONS.values() for fw in ecus[Ecu.fwdRadar, 0x757, None]})
for platform in CAR:
- with self.subTest(platform=platform):
+ with subtests.test(platform=platform.name):
for wmi in WMI:
for chassis_code in platform.config.chassis_codes | {"00"}:
vin = ["0"] * 17
@@ -59,8 +58,4 @@ class TestVolkswagenPlatformConfigs(unittest.TestCase):
matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fws, vin, FW_VERSIONS)
expected_matches = {platform} if should_match else set()
- self.assertEqual(expected_matches, matches, "Bad match")
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert expected_matches == matches, "Bad match"
diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py
index eb537575fa..4d317176be 100644
--- a/selfdrive/car/volkswagen/values.py
+++ b/selfdrive/car/volkswagen/values.py
@@ -214,10 +214,11 @@ class CAR(Platforms):
VWCarDocs("Volkswagen Arteon 2018-23", video_link="https://youtu.be/FAomFKPFlDA"),
VWCarDocs("Volkswagen Arteon R 2020-23", video_link="https://youtu.be/FAomFKPFlDA"),
VWCarDocs("Volkswagen Arteon eHybrid 2020-23", video_link="https://youtu.be/FAomFKPFlDA"),
+ VWCarDocs("Volkswagen Arteon Shooting Brake 2020-23", video_link="https://youtu.be/FAomFKPFlDA"),
VWCarDocs("Volkswagen CC 2018-22", video_link="https://youtu.be/FAomFKPFlDA"),
],
VolkswagenCarSpecs(mass=1733, wheelbase=2.84),
- chassis_codes={"AN"},
+ chassis_codes={"AN", "3H"},
wmis={WMI.VOLKSWAGEN_EUROPE_CAR},
)
VOLKSWAGEN_ATLAS_MK1 = VolkswagenMQBPlatformConfig(
diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py
index 53332acde9..e2ed9673ca 100755
--- a/selfdrive/controls/controlsd.py
+++ b/selfdrive/controls/controlsd.py
@@ -18,8 +18,7 @@ from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper, DT_CTRL
from openpilot.common.swaglog import cloudlog
-from openpilot.selfdrive.car.car_helpers import get_startup_event
-from openpilot.selfdrive.car.card import CarD
+from openpilot.selfdrive.car.car_helpers import get_car_interface, get_startup_event
from openpilot.selfdrive.controls.lib.alertmanager import AlertManager, set_offroad_alert
from openpilot.selfdrive.controls.lib.drive_helpers import VCruiseHelper, clip_curvature
from openpilot.selfdrive.controls.lib.events import Events, ET
@@ -61,16 +60,19 @@ ENABLED_STATES = (State.preEnabled, *ACTIVE_STATES)
class Controls:
def __init__(self, CI=None):
- self.card = CarD(CI)
-
self.params = Params()
- with car.CarParams.from_bytes(self.params.get("CarParams", block=True)) as msg:
- # TODO: this shouldn't need to be a builder
- self.CP = msg.as_builder()
-
- self.CI = self.card.CI
+ if CI is None:
+ cloudlog.info("controlsd is waiting for CarParams")
+ with car.CarParams.from_bytes(self.params.get("CarParams", block=True)) as msg:
+ # TODO: this shouldn't need to be a builder
+ self.CP = msg.as_builder()
+ cloudlog.info("controlsd got CarParams")
+ # Uses car interface helper functions, altering state won't be considered by card for actuation
+ self.CI = get_car_interface(self.CP)
+ else:
+ self.CI, self.CP = CI, CI.CP
# Ensure the current branch is cached, otherwise the first iteration of controlsd lags
self.branch = get_short_branch()
@@ -83,9 +85,15 @@ class Controls:
self.log_sock = messaging.sub_sock('androidLog')
+ # TODO: de-couple controlsd with card/conflate on carState without introducing controls mismatches
+ self.car_state_sock = messaging.sub_sock('carState', timeout=20)
+
ignore = self.sensor_packets + ['testJoystick']
if SIMULATION:
ignore += ['driverCameraState', 'managerState']
+ if REPLAY:
+ # no vipc in replay will make them ignored anyways
+ ignore += ['roadCameraState', 'wideRoadCameraState']
self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration',
'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'liveLocationKalman',
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
@@ -96,7 +104,6 @@ class Controls:
self.joystick_mode = self.params.get_bool("JoystickDebugMode")
# read params
- self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
self.is_metric = self.params.get_bool("IsMetric")
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
@@ -142,7 +149,6 @@ class Controls:
self.logged_comm_issue = None
self.not_running_prev = None
self.steer_limited = False
- self.last_actuators = car.CarControl.Actuators.new_message()
self.desired_curvature = 0.0
self.experimental_mode = False
self.personality = self.read_personality_param()
@@ -166,12 +172,12 @@ class Controls:
elif self.CP.passive:
self.events.add(EventName.dashcamMode, static=True)
- # controlsd is driven by can recv, expected at 100Hz
+ # controlsd is driven by carState, expected at 100Hz
self.rk = Ratekeeper(100, print_delay_threshold=None)
def set_initial_state(self):
if REPLAY:
- controls_state = Params().get("ReplayControlsState")
+ controls_state = self.params.get("ReplayControlsState")
if controls_state is not None:
with log.ControlsState.from_bytes(controls_state) as controls_state:
self.v_cruise_helper.v_cruise_kph = controls_state.vCruise
@@ -208,18 +214,6 @@ class Controls:
if not self.CP.pcmCruise and not self.v_cruise_helper.v_cruise_initialized and resume_pressed:
self.events.add(EventName.resumeBlocked)
- # Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0
- if (CS.gasPressed and not self.CS_prev.gasPressed and self.disengage_on_accelerator) or \
- (CS.brakePressed and (not self.CS_prev.brakePressed or not CS.standstill)) or \
- (CS.regenBraking and (not self.CS_prev.regenBraking or not CS.standstill)):
- self.events.add(EventName.pedalPressed)
-
- if CS.brakePressed and CS.standstill:
- self.events.add(EventName.preEnableStandstill)
-
- if CS.gasPressed:
- self.events.add(EventName.gasPressedOverride)
-
if not self.CP.notCar:
self.events.add_from_msg(self.sm['driverMonitoringState'].events)
self.events.add_from_msg(self.sm['longitudinalPlanSP'].events)
@@ -318,7 +312,7 @@ class Controls:
self.events.add(EventName.cameraFrameRate)
if not REPLAY and self.rk.lagging:
self.events.add(EventName.controlsdLagging)
- if len(self.sm['radarState'].radarErrors) or (not self.rk.lagging and not self.sm.all_checks(['radarState'])):
+ if len(self.sm['radarState'].radarErrors) or ((not self.rk.lagging or REPLAY) and not self.sm.all_checks(['radarState'])):
self.events.add(EventName.radarFault)
if not self.sm.valid['pandaStates']:
self.events.add(EventName.usbError)
@@ -330,7 +324,7 @@ class Controls:
# generic catch-all. ideally, a more specific event should be added above instead
has_disable_events = self.events.contains(ET.NO_ENTRY) and (self.events.contains(ET.SOFT_DISABLE) or self.events.contains(ET.IMMEDIATE_DISABLE))
no_system_errors = (not has_disable_events) or (len(self.events) == num_events)
- if (not self.sm.all_checks() or self.card.can_rcv_timeout) and no_system_errors:
+ if (not self.sm.all_checks() or CS.canRcvTimeout) and no_system_errors:
if not self.sm.all_alive():
self.events.add(EventName.commIssue)
elif not self.sm.all_freq_ok():
@@ -342,7 +336,7 @@ class Controls:
'invalid': [s for s, valid in self.sm.valid.items() if not valid],
'not_alive': [s for s, alive in self.sm.alive.items() if not alive],
'not_freq_ok': [s for s, freq_ok in self.sm.freq_ok.items() if not freq_ok],
- 'can_rcv_timeout': self.card.can_rcv_timeout,
+ 'can_rcv_timeout': CS.canRcvTimeout,
}
if logs != self.logged_comm_issue:
cloudlog.event("commIssue", error=True, **logs)
@@ -402,9 +396,10 @@ class Controls:
self.events.add(EventName.modeldLagging)
def data_sample(self):
- """Receive data from sockets and update carState"""
+ """Receive data from sockets"""
- CS = self.card.state_update()
+ car_state = messaging.recv_one(self.car_state_sock)
+ CS = car_state.carState if car_state else self.CS_prev
self.sm.update(0)
@@ -418,12 +413,8 @@ class Controls:
if VisionStreamType.VISION_STREAM_WIDE_ROAD not in available_streams:
self.sm.ignore_alive.append('wideRoadCameraState')
- if not self.CP.passive:
- self.card.initialize()
-
self.initialized = True
self.set_initial_state()
- self.params.put_bool_nonblocking("ControlsReady", True)
cloudlog.event(
"controlsd.initialized",
@@ -627,7 +618,7 @@ class Controls:
undershooting = abs(lac_log.desiredLateralAccel) / abs(1e-3 + lac_log.actualLateralAccel) > 1.2
turning = abs(lac_log.desiredLateralAccel) > 1.0
good_speed = CS.vEgo > 5
- max_torque = abs(self.last_actuators.steer) > 0.99
+ max_torque = abs(self.sm['carOutput'].actuatorsOutput.steer) > 0.99
if undershooting and turning and good_speed and max_torque:
lac_log.active and self.events.add(EventName.steerSaturated)
elif lac_log.saturated:
@@ -668,8 +659,6 @@ class Controls:
def publish_logs(self, CS, start_time, CC, lac_log):
"""Send actuators and hud commands to the car, send controlsstate and MPC logging"""
- CO = self.sm['carOutput']
-
# Orientation and angle rates can be useful for carcontroller
# Only calibrated (car) frame is relevant for the carcontroller
orientation_value = list(self.sm['liveLocationKalman'].calibratedOrientationNED.value)
@@ -733,8 +722,7 @@ class Controls:
hudControl.visualAlert = current_alert.visual_alert
if not self.CP.passive and self.initialized:
- self.card.controls_update(CC)
- self.last_actuators = CO.actuatorsOutput
+ CO = self.sm['carOutput']
if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
self.steer_limited = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \
STEER_ANGLE_SATURATION_THRESHOLD
@@ -781,7 +769,6 @@ class Controls:
controlsState.cumLagMs = -self.rk.remaining * 1000.
controlsState.startMonoTime = int(start_time * 1e9)
controlsState.forceDecel = bool(force_decel)
- controlsState.canErrorCounter = self.card.can_rcv_cum_timeout_counter
controlsState.experimentalMode = self.experimental_mode
controlsState.personality = self.personality
diff --git a/selfdrive/controls/lib/events.py b/selfdrive/controls/lib/events.py
index e3398304cf..6c39376cfd 100755
--- a/selfdrive/controls/lib/events.py
+++ b/selfdrive/controls/lib/events.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
+import bisect
import math
import os
from enum import IntEnum
@@ -61,8 +62,8 @@ class Events:
def add(self, event_name: int, static: bool=False) -> None:
if static:
- self.static_events.append(event_name)
- self.events.append(event_name)
+ bisect.insort(self.static_events, event_name)
+ bisect.insort(self.events, event_name)
def clear(self) -> None:
self.events_prev = {k: (v + 1 if k in self.events else 0) for k, v in self.events_prev.items()}
@@ -92,7 +93,7 @@ class Events:
def add_from_msg(self, events):
for e in events:
- self.events.append(e.name.raw)
+ bisect.insort(self.events, e.name.raw)
def to_msg(self):
ret = []
@@ -340,6 +341,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# ********** events with no alerts **********
EventName.stockFcw: {},
+ EventName.actuatorsApiUnavailable: {},
# ********** events only containing alerts displayed in all states **********
@@ -516,7 +518,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
"Press Resume to Exit Standstill",
"",
AlertStatus.userPrompt, AlertSize.small,
- Priority.MID, VisualAlert.none, AudibleAlert.none, .2),
+ Priority.LOW, VisualAlert.none, AudibleAlert.none, .2),
},
EventName.belowSteerSpeed: {
diff --git a/selfdrive/controls/lib/lateral_mpc_lib/lat_mpc.py b/selfdrive/controls/lib/lateral_mpc_lib/lat_mpc.py
index 83ec3b3a13..ad60861088 100755
--- a/selfdrive/controls/lib/lateral_mpc_lib/lat_mpc.py
+++ b/selfdrive/controls/lib/lateral_mpc_lib/lat_mpc.py
@@ -128,7 +128,7 @@ def gen_lat_ocp():
return ocp
-class LateralMpc():
+class LateralMpc:
def __init__(self, x0=None):
if x0 is None:
x0 = np.zeros(X_DIM)
diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py
index 39cfda4e8a..bad37add63 100755
--- a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py
+++ b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py
@@ -4,6 +4,7 @@ import time
import numpy as np
from cereal import log
from openpilot.common.numpy_fast import clip
+from openpilot.common.realtime import DT_MDL
from openpilot.common.swaglog import cloudlog
# WARNING: imports outside of constants will not trigger a rebuild
from openpilot.selfdrive.modeld.constants import index_function
@@ -220,8 +221,9 @@ def gen_long_ocp():
class LongitudinalMpc:
- def __init__(self, mode='acc'):
+ def __init__(self, mode='acc', dt=DT_MDL):
self.mode = mode
+ self.dt = dt
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
self.reset()
self.source = SOURCES[2]
@@ -440,7 +442,7 @@ class LongitudinalMpc:
self.a_solution = self.x_sol[:,2]
self.j_solution = self.u_sol[:,0]
- self.prev_a = np.interp(T_IDXS + 0.05, T_IDXS, self.a_solution)
+ self.prev_a = np.interp(T_IDXS + self.dt, T_IDXS, self.a_solution)
t = time.monotonic()
if self.solution_status != 0:
diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py
index 1aa6e76a0b..a975ad727b 100755
--- a/selfdrive/controls/lib/longitudinal_planner.py
+++ b/selfdrive/controls/lib/longitudinal_planner.py
@@ -48,7 +48,7 @@ def limit_accel_in_turns(v_ego, angle_steers, a_target, CP):
class LongitudinalPlanner:
def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
self.CP = CP
- self.mpc = LongitudinalMpc()
+ self.mpc = LongitudinalMpc(dt=dt)
self.fcw = False
self.dt = dt
diff --git a/selfdrive/controls/lib/pid.py b/selfdrive/controls/lib/pid.py
index f4ec7e5996..29c4d8bd46 100644
--- a/selfdrive/controls/lib/pid.py
+++ b/selfdrive/controls/lib/pid.py
@@ -4,7 +4,7 @@ from numbers import Number
from openpilot.common.numpy_fast import clip, interp
-class PIDController():
+class PIDController:
def __init__(self, k_p, k_i, k_f=0., k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100):
self._k_p = k_p
self._k_i = k_i
diff --git a/selfdrive/controls/lib/tests/test_alertmanager.py b/selfdrive/controls/lib/tests/test_alertmanager.py
index dbd42858a0..c234cc49d6 100755
--- a/selfdrive/controls/lib/tests/test_alertmanager.py
+++ b/selfdrive/controls/lib/tests/test_alertmanager.py
@@ -1,12 +1,11 @@
#!/usr/bin/env python3
import random
-import unittest
from openpilot.selfdrive.controls.lib.events import Alert, EVENTS
from openpilot.selfdrive.controls.lib.alertmanager import AlertManager
-class TestAlertManager(unittest.TestCase):
+class TestAlertManager:
def test_duration(self):
"""
@@ -38,8 +37,4 @@ class TestAlertManager(unittest.TestCase):
shown = current_alert is not None
should_show = frame <= show_duration
- self.assertEqual(shown, should_show, msg=f"{frame=} {add_duration=} {duration=}")
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert shown == should_show, f"{frame=} {add_duration=} {duration=}"
diff --git a/selfdrive/controls/lib/tests/test_latcontrol.py b/selfdrive/controls/lib/tests/test_latcontrol.py
index 838023af72..b731bbd950 100755
--- a/selfdrive/controls/lib/tests/test_latcontrol.py
+++ b/selfdrive/controls/lib/tests/test_latcontrol.py
@@ -1,6 +1,4 @@
#!/usr/bin/env python3
-import unittest
-
from parameterized import parameterized
from cereal import car, log
@@ -15,7 +13,7 @@ from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel
from openpilot.common.mock.generators import generate_liveLocationKalman
-class TestLatControl(unittest.TestCase):
+class TestLatControl:
@parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque), (NISSAN.NISSAN_LEAF, LatControlAngle)])
def test_saturation(self, car_name, controller):
@@ -36,8 +34,4 @@ class TestLatControl(unittest.TestCase):
for _ in range(1000):
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, llk)
- self.assertTrue(lac_log.saturated)
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert lac_log.saturated
diff --git a/selfdrive/controls/lib/tests/test_vehicle_model.py b/selfdrive/controls/lib/tests/test_vehicle_model.py
index c3997afdf3..2efcf2fbbd 100755
--- a/selfdrive/controls/lib/tests/test_vehicle_model.py
+++ b/selfdrive/controls/lib/tests/test_vehicle_model.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
+import pytest
import math
-import unittest
import numpy as np
from control import StateSpace
@@ -10,8 +10,8 @@ from openpilot.selfdrive.car.honda.values import CAR
from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel, dyn_ss_sol, create_dyn_state_matrices
-class TestVehicleModel(unittest.TestCase):
- def setUp(self):
+class TestVehicleModel:
+ def setup_method(self):
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
self.VM = VehicleModel(CP)
@@ -23,7 +23,7 @@ class TestVehicleModel(unittest.TestCase):
yr = self.VM.yaw_rate(sa, u, roll)
new_sa = self.VM.get_steer_from_yaw_rate(yr, u, roll)
- self.assertAlmostEqual(sa, new_sa)
+ assert sa == pytest.approx(new_sa)
def test_dyn_ss_sol_against_yaw_rate(self):
"""Verify that the yaw_rate helper function matches the results
@@ -38,7 +38,7 @@ class TestVehicleModel(unittest.TestCase):
# Compute yaw rate using direct computations
yr2 = self.VM.yaw_rate(sa, u, roll)
- self.assertAlmostEqual(float(yr1[0]), yr2)
+ assert float(yr1[0]) == pytest.approx(yr2)
def test_syn_ss_sol_simulate(self):
"""Verifies that dyn_ss_sol matches a simulation"""
@@ -63,8 +63,3 @@ class TestVehicleModel(unittest.TestCase):
x2 = dyn_ss_sol(sa, u, roll, self.VM)
np.testing.assert_almost_equal(x1, x2, decimal=3)
-
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/controls/radard.py b/selfdrive/controls/radard.py
index 16c9e0635c..7420f666f7 100755
--- a/selfdrive/controls/radard.py
+++ b/selfdrive/controls/radard.py
@@ -2,7 +2,7 @@
import importlib
import math
from collections import deque
-from typing import Any, Optional
+from typing import Any
import capnp
from cereal import messaging, log, car
@@ -208,7 +208,7 @@ class RadarD:
self.ready = False
- def update(self, sm: messaging.SubMaster, rr: Optional[car.RadarData]):
+ def update(self, sm: messaging.SubMaster, rr):
self.ready = sm.seen['modelV2']
self.current_time = 1e-9*max(sm.logMonoTime.values())
diff --git a/selfdrive/controls/tests/test_alerts.py b/selfdrive/controls/tests/test_alerts.py
index 7b4fba0dce..e29a6322ab 100755
--- a/selfdrive/controls/tests/test_alerts.py
+++ b/selfdrive/controls/tests/test_alerts.py
@@ -2,7 +2,6 @@
import copy
import json
import os
-import unittest
import random
from PIL import Image, ImageDraw, ImageFont
@@ -25,10 +24,10 @@ for event_types in EVENTS.values():
ALERTS.append(alert)
-class TestAlerts(unittest.TestCase):
+class TestAlerts:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
with open(OFFROAD_ALERTS_PATH) as f:
cls.offroad_alerts = json.loads(f.read())
@@ -45,7 +44,7 @@ class TestAlerts(unittest.TestCase):
for name, e in events.items():
if not name.endswith("DEPRECATED"):
fail_msg = "%s @%d not in EVENTS" % (name, e)
- self.assertTrue(e in EVENTS.keys(), msg=fail_msg)
+ assert e in EVENTS.keys(), fail_msg
# ensure alert text doesn't exceed allowed width
def test_alert_text_length(self):
@@ -80,7 +79,7 @@ class TestAlerts(unittest.TestCase):
left, _, right, _ = draw.textbbox((0, 0), txt, font)
width = right - left
msg = f"type: {alert.alert_type} msg: {txt}"
- self.assertLessEqual(width, max_text_width, msg=msg)
+ assert width <= max_text_width, msg
def test_alert_sanity_check(self):
for event_types in EVENTS.values():
@@ -90,21 +89,21 @@ class TestAlerts(unittest.TestCase):
continue
if a.alert_size == AlertSize.none:
- self.assertEqual(len(a.alert_text_1), 0)
- self.assertEqual(len(a.alert_text_2), 0)
+ assert len(a.alert_text_1) == 0
+ assert len(a.alert_text_2) == 0
elif a.alert_size == AlertSize.small:
- self.assertGreater(len(a.alert_text_1), 0)
- self.assertEqual(len(a.alert_text_2), 0)
+ assert len(a.alert_text_1) > 0
+ assert len(a.alert_text_2) == 0
elif a.alert_size == AlertSize.mid:
- self.assertGreater(len(a.alert_text_1), 0)
- self.assertGreater(len(a.alert_text_2), 0)
+ assert len(a.alert_text_1) > 0
+ assert len(a.alert_text_2) > 0
else:
- self.assertGreater(len(a.alert_text_1), 0)
+ assert len(a.alert_text_1) > 0
- self.assertGreaterEqual(a.duration, 0.)
+ assert a.duration >= 0.
if event_type not in (ET.WARNING, ET.PERMANENT, ET.PRE_ENABLE):
- self.assertEqual(a.creation_delay, 0.)
+ assert a.creation_delay == 0.
def test_offroad_alerts(self):
params = Params()
@@ -113,11 +112,11 @@ class TestAlerts(unittest.TestCase):
alert = copy.copy(self.offroad_alerts[a])
set_offroad_alert(a, True)
alert['extra'] = ''
- self.assertTrue(json.dumps(alert) == params.get(a, encoding='utf8'))
+ assert json.dumps(alert) == params.get(a, encoding='utf8')
# then delete it
set_offroad_alert(a, False)
- self.assertTrue(params.get(a) is None)
+ assert params.get(a) is None
def test_offroad_alerts_extra_text(self):
params = Params()
@@ -128,8 +127,5 @@ class TestAlerts(unittest.TestCase):
set_offroad_alert(a, True, extra_text="a"*i)
written_alert = json.loads(params.get(a, encoding='utf8'))
- self.assertTrue("a"*i == written_alert['extra'])
- self.assertTrue(alert["text"] == written_alert['text'])
-
-if __name__ == "__main__":
- unittest.main()
+ assert "a"*i == written_alert['extra']
+ assert alert["text"] == written_alert['text']
diff --git a/selfdrive/controls/tests/test_cruise_speed.py b/selfdrive/controls/tests/test_cruise_speed.py
index c46d03ad1e..6c46285e81 100755
--- a/selfdrive/controls/tests/test_cruise_speed.py
+++ b/selfdrive/controls/tests/test_cruise_speed.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
+import pytest
import itertools
import numpy as np
-import unittest
from parameterized import parameterized_class
from cereal import log
@@ -36,19 +36,19 @@ def run_cruise_simulation(cruise, e2e, personality, t_end=20.):
[True, False], # e2e
log.LongitudinalPersonality.schema.enumerants, # personality
[5,35])) # speed
-class TestCruiseSpeed(unittest.TestCase):
+class TestCruiseSpeed:
def test_cruise_speed(self):
print(f'Testing {self.speed} m/s')
cruise_speed = float(self.speed)
simulation_steady_state = run_cruise_simulation(cruise_speed, self.e2e, self.personality)
- self.assertAlmostEqual(simulation_steady_state, cruise_speed, delta=.01, msg=f'Did not reach {self.speed} m/s')
+ assert simulation_steady_state == pytest.approx(cruise_speed, abs=.01), f'Did not reach {self.speed} m/s'
# TODO: test pcmCruise
@parameterized_class(('pcm_cruise',), [(False,)])
-class TestVCruiseHelper(unittest.TestCase):
- def setUp(self):
+class TestVCruiseHelper:
+ def setup_method(self):
self.CP = car.CarParams(pcmCruise=self.pcm_cruise)
self.v_cruise_helper = VCruiseHelper(self.CP)
self.reset_cruise_speed_state()
@@ -75,7 +75,7 @@ class TestVCruiseHelper(unittest.TestCase):
CS.buttonEvents = [ButtonEvent(type=btn, pressed=pressed)]
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False)
- self.assertEqual(pressed, self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
+ assert pressed == (self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
def test_rising_edge_enable(self):
"""
@@ -94,7 +94,7 @@ class TestVCruiseHelper(unittest.TestCase):
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False)
# Expected diff on enabling. Speed should not change on falling edge of pressed
- self.assertEqual(not pressed, self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
+ assert not pressed == self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last
def test_resume_in_standstill(self):
"""
@@ -111,7 +111,7 @@ class TestVCruiseHelper(unittest.TestCase):
# speed should only update if not at standstill and button falling edge
should_equal = standstill or pressed
- self.assertEqual(should_equal, self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
+ assert should_equal == (self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
def test_set_gas_pressed(self):
"""
@@ -135,7 +135,7 @@ class TestVCruiseHelper(unittest.TestCase):
# TODO: fix skipping first run due to enabled on rising edge exception
if v_ego == 0.0:
continue
- self.assertEqual(expected_v_cruise_kph, self.v_cruise_helper.v_cruise_kph)
+ assert expected_v_cruise_kph == self.v_cruise_helper.v_cruise_kph
def test_initialize_v_cruise(self):
"""
@@ -145,12 +145,8 @@ class TestVCruiseHelper(unittest.TestCase):
for experimental_mode in (True, False):
for v_ego in np.linspace(0, 100, 101):
self.reset_cruise_speed_state()
- self.assertFalse(self.v_cruise_helper.v_cruise_initialized)
+ assert not self.v_cruise_helper.v_cruise_initialized
self.enable(float(v_ego), experimental_mode)
- self.assertTrue(V_CRUISE_INITIAL <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX)
- self.assertTrue(self.v_cruise_helper.v_cruise_initialized)
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert V_CRUISE_INITIAL <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX
+ assert self.v_cruise_helper.v_cruise_initialized
diff --git a/selfdrive/controls/tests/test_following_distance.py b/selfdrive/controls/tests/test_following_distance.py
index f58e6383c4..89a575a9ad 100755
--- a/selfdrive/controls/tests/test_following_distance.py
+++ b/selfdrive/controls/tests/test_following_distance.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-import unittest
+import pytest
import itertools
from parameterized import parameterized_class
@@ -32,14 +32,10 @@ def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personalit
log.LongitudinalPersonality.standard,
log.LongitudinalPersonality.aggressive],
[0,10,35])) # speed
-class TestFollowingDistance(unittest.TestCase):
+class TestFollowingDistance:
def test_following_distance(self):
v_lead = float(self.speed)
simulation_steady_state = run_following_distance_simulation(v_lead, e2e=self.e2e, personality=self.personality)
correct_steady_state = desired_follow_distance(v_lead, v_lead, get_T_FOLLOW(self.personality))
err_ratio = 0.2 if self.e2e else 0.1
- self.assertAlmostEqual(simulation_steady_state, correct_steady_state, delta=(err_ratio * correct_steady_state + .5))
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert simulation_steady_state == pytest.approx(correct_steady_state, abs=err_ratio * correct_steady_state + .5)
diff --git a/selfdrive/controls/tests/test_lateral_mpc.py b/selfdrive/controls/tests/test_lateral_mpc.py
index 8c09f46b60..3aa0fd1bce 100644
--- a/selfdrive/controls/tests/test_lateral_mpc.py
+++ b/selfdrive/controls/tests/test_lateral_mpc.py
@@ -1,4 +1,4 @@
-import unittest
+import pytest
import numpy as np
from openpilot.selfdrive.controls.lib.lateral_mpc_lib.lat_mpc import LateralMpc
from openpilot.selfdrive.controls.lib.drive_helpers import CAR_ROTATION_RADIUS
@@ -27,20 +27,20 @@ def run_mpc(lat_mpc=None, v_ref=30., x_init=0., y_init=0., psi_init=0., curvatur
return lat_mpc.x_sol
-class TestLateralMpc(unittest.TestCase):
+class TestLateralMpc:
def _assert_null(self, sol, curvature=1e-6):
for i in range(len(sol)):
- self.assertAlmostEqual(sol[0,i,1], 0., delta=curvature)
- self.assertAlmostEqual(sol[0,i,2], 0., delta=curvature)
- self.assertAlmostEqual(sol[0,i,3], 0., delta=curvature)
+ assert sol[0,i,1] == pytest.approx(0, abs=curvature)
+ assert sol[0,i,2] == pytest.approx(0, abs=curvature)
+ assert sol[0,i,3] == pytest.approx(0, abs=curvature)
def _assert_simmetry(self, sol, curvature=1e-6):
for i in range(len(sol)):
- self.assertAlmostEqual(sol[0,i,1], -sol[1,i,1], delta=curvature)
- self.assertAlmostEqual(sol[0,i,2], -sol[1,i,2], delta=curvature)
- self.assertAlmostEqual(sol[0,i,3], -sol[1,i,3], delta=curvature)
- self.assertAlmostEqual(sol[0,i,0], sol[1,i,0], delta=curvature)
+ assert sol[0,i,1] == pytest.approx(-sol[1,i,1], abs=curvature)
+ assert sol[0,i,2] == pytest.approx(-sol[1,i,2], abs=curvature)
+ assert sol[0,i,3] == pytest.approx(-sol[1,i,3], abs=curvature)
+ assert sol[0,i,0] == pytest.approx(sol[1,i,0], abs=curvature)
def test_straight(self):
sol = run_mpc()
@@ -74,7 +74,7 @@ class TestLateralMpc(unittest.TestCase):
y_init = 1.
sol = run_mpc(y_init=y_init)
for y in list(sol[:,1]):
- self.assertGreaterEqual(y_init, abs(y))
+ assert y_init >= abs(y)
def test_switch_convergence(self):
lat_mpc = LateralMpc()
@@ -83,7 +83,3 @@ class TestLateralMpc(unittest.TestCase):
sol = run_mpc(lat_mpc=lat_mpc, poly_shift=-3.0, v_ref=7.0)
left_psi_deg = np.degrees(sol[:,2])
np.testing.assert_almost_equal(right_psi_deg, -left_psi_deg, decimal=3)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/controls/tests/test_leads.py b/selfdrive/controls/tests/test_leads.py
index a06387a087..f4e97725ff 100755
--- a/selfdrive/controls/tests/test_leads.py
+++ b/selfdrive/controls/tests/test_leads.py
@@ -1,13 +1,11 @@
#!/usr/bin/env python3
-import unittest
-
import cereal.messaging as messaging
from openpilot.selfdrive.test.process_replay import replay_process_with_name
from openpilot.selfdrive.car.toyota.values import CAR as TOYOTA
-class TestLeads(unittest.TestCase):
+class TestLeads:
def test_radar_fault(self):
# if there's no radar-related can traffic, radard should either not respond or respond with an error
# this is tightly coupled with underlying car radar_interface implementation, but it's a good sanity check
@@ -29,8 +27,4 @@ class TestLeads(unittest.TestCase):
states = [m for m in out if m.which() == "radarState"]
failures = [not state.valid and len(state.radarState.radarErrors) for state in states]
- self.assertTrue(len(states) == 0 or all(failures))
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert len(states) == 0 or all(failures)
diff --git a/selfdrive/controls/tests/test_startup.py b/selfdrive/controls/tests/test_startup.py
index 23cc96a2e4..fc9f8ab125 100644
--- a/selfdrive/controls/tests/test_startup.py
+++ b/selfdrive/controls/tests/test_startup.py
@@ -9,7 +9,7 @@ from openpilot.selfdrive.car.fingerprints import _FINGERPRINTS
from openpilot.selfdrive.car.toyota.values import CAR as TOYOTA
from openpilot.selfdrive.car.mazda.values import CAR as MAZDA
from openpilot.selfdrive.controls.lib.events import EVENT_NAME
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
EventName = car.CarEvent.EventName
Ecu = car.CarParams.Ecu
@@ -87,6 +87,7 @@ def test_startup_alert(expected_event, car_model, fw_versions, brand):
os.environ['SKIP_FW_QUERY'] = '1'
managed_processes['controlsd'].start()
+ managed_processes['card'].start()
assert pm.wait_for_readers_to_update('can', 5)
pm.send('can', can_list_to_can_capnp([[0, 0, b"", 0]]))
@@ -104,7 +105,7 @@ def test_startup_alert(expected_event, car_model, fw_versions, brand):
msgs = [[addr, 0, b'\x00'*length, 0] for addr, length in finger.items()]
for _ in range(1000):
- # controlsd waits for boardd to echo back that it has changed the multiplexing mode
+ # card waits for boardd to echo back that it has changed the multiplexing mode
if not params.get_bool("ObdMultiplexingChanged"):
params.put_bool("ObdMultiplexingChanged", True)
diff --git a/selfdrive/controls/tests/test_state_machine.py b/selfdrive/controls/tests/test_state_machine.py
index d49111752d..b92724ce43 100755
--- a/selfdrive/controls/tests/test_state_machine.py
+++ b/selfdrive/controls/tests/test_state_machine.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
-import unittest
from cereal import car, log
from openpilot.common.realtime import DT_CTRL
@@ -28,9 +27,9 @@ def make_event(event_types):
return 0
-class TestStateMachine(unittest.TestCase):
+class TestStateMachine:
- def setUp(self):
+ def setup_method(self):
CarInterface, CarController, CarState = interfaces[MOCK.MOCK]
CP = CarInterface.get_non_essential_params(MOCK.MOCK)
CI = CarInterface(CP, CarController, CarState)
@@ -46,7 +45,7 @@ class TestStateMachine(unittest.TestCase):
self.controlsd.events.add(make_event([et, ET.IMMEDIATE_DISABLE]))
self.controlsd.state = state
self.controlsd.state_transition(self.CS)
- self.assertEqual(State.disabled, self.controlsd.state)
+ assert State.disabled == self.controlsd.state
self.controlsd.events.clear()
def test_user_disable(self):
@@ -55,7 +54,7 @@ class TestStateMachine(unittest.TestCase):
self.controlsd.events.add(make_event([et, ET.USER_DISABLE]))
self.controlsd.state = state
self.controlsd.state_transition(self.CS)
- self.assertEqual(State.disabled, self.controlsd.state)
+ assert State.disabled == self.controlsd.state
self.controlsd.events.clear()
def test_soft_disable(self):
@@ -66,7 +65,7 @@ class TestStateMachine(unittest.TestCase):
self.controlsd.events.add(make_event([et, ET.SOFT_DISABLE]))
self.controlsd.state = state
self.controlsd.state_transition(self.CS)
- self.assertEqual(self.controlsd.state, State.disabled if state == State.disabled else State.softDisabling)
+ assert self.controlsd.state == State.disabled if state == State.disabled else State.softDisabling
self.controlsd.events.clear()
def test_soft_disable_timer(self):
@@ -74,17 +73,17 @@ class TestStateMachine(unittest.TestCase):
self.controlsd.events.add(make_event([ET.SOFT_DISABLE]))
self.controlsd.state_transition(self.CS)
for _ in range(int(SOFT_DISABLE_TIME / DT_CTRL)):
- self.assertEqual(self.controlsd.state, State.softDisabling)
+ assert self.controlsd.state == State.softDisabling
self.controlsd.state_transition(self.CS)
- self.assertEqual(self.controlsd.state, State.disabled)
+ assert self.controlsd.state == State.disabled
def test_no_entry(self):
# Make sure noEntry keeps us disabled
for et in ENABLE_EVENT_TYPES:
self.controlsd.events.add(make_event([ET.NO_ENTRY, et]))
self.controlsd.state_transition(self.CS)
- self.assertEqual(self.controlsd.state, State.disabled)
+ assert self.controlsd.state == State.disabled
self.controlsd.events.clear()
def test_no_entry_pre_enable(self):
@@ -92,7 +91,7 @@ class TestStateMachine(unittest.TestCase):
self.controlsd.state = State.preEnabled
self.controlsd.events.add(make_event([ET.NO_ENTRY, ET.PRE_ENABLE]))
self.controlsd.state_transition(self.CS)
- self.assertEqual(self.controlsd.state, State.preEnabled)
+ assert self.controlsd.state == State.preEnabled
def test_maintain_states(self):
# Given current state's event type, we should maintain state
@@ -101,9 +100,5 @@ class TestStateMachine(unittest.TestCase):
self.controlsd.state = state
self.controlsd.events.add(make_event([et]))
self.controlsd.state_transition(self.CS)
- self.assertEqual(self.controlsd.state, state)
+ assert self.controlsd.state == state
self.controlsd.events.clear()
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/debug/cpu_usage_stat.py b/selfdrive/debug/cpu_usage_stat.py
index ec9d02e8f4..baf44b9082 100755
--- a/selfdrive/debug/cpu_usage_stat.py
+++ b/selfdrive/debug/cpu_usage_stat.py
@@ -24,7 +24,7 @@ import argparse
import re
from collections import defaultdict
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
# Do statistics every 5 seconds
PRINT_INTERVAL = 5
diff --git a/selfdrive/debug/cycle_alerts.py b/selfdrive/debug/cycle_alerts.py
index d44c70ddb9..c0f92cde18 100755
--- a/selfdrive/debug/cycle_alerts.py
+++ b/selfdrive/debug/cycle_alerts.py
@@ -8,7 +8,7 @@ from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.car.honda.interface import CarInterface
from openpilot.selfdrive.controls.lib.events import ET, Events
from openpilot.selfdrive.controls.lib.alertmanager import AlertManager
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
EventName = car.CarEvent.EventName
diff --git a/selfdrive/debug/debug_fw_fingerprinting_offline.py b/selfdrive/debug/debug_fw_fingerprinting_offline.py
index 8ae9d6d347..3df2924738 100755
--- a/selfdrive/debug/debug_fw_fingerprinting_offline.py
+++ b/selfdrive/debug/debug_fw_fingerprinting_offline.py
@@ -1,11 +1,12 @@
#!/usr/bin/env python3
import argparse
+from openpilot.tools.lib.live_logreader import live_logreader
from openpilot.tools.lib.logreader import LogReader, ReadMode
from panda.python import uds
-def main(route: str, addrs: list[int]):
+def main(route: str | None, addrs: list[int]):
"""
TODO:
- highlight TX vs RX clearly
@@ -13,7 +14,10 @@ def main(route: str, addrs: list[int]):
- print as fixed width table, easier to read
"""
- lr = LogReader(route, default_mode=ReadMode.RLOG, sort_by_time=True)
+ if route is None:
+ lr = live_logreader()
+ else:
+ lr = LogReader(route, default_mode=ReadMode.RLOG, sort_by_time=True)
start_mono_time = None
prev_mono_time = 0
@@ -28,7 +32,7 @@ def main(route: str, addrs: list[int]):
if msg.which() in ("can", 'sendcan'):
for can in getattr(msg, msg.which()):
- if can.address in addrs:
+ if can.address in addrs or not len(addrs):
if msg.logMonoTime != prev_mono_time:
print()
prev_mono_time = msg.logMonoTime
@@ -38,8 +42,8 @@ def main(route: str, addrs: list[int]):
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='View back and forth ISO-TP communication between various ECUs given an address')
- parser.add_argument('route', help='Route name')
- parser.add_argument('addrs', nargs='*', help='List of tx address to view (0x7e0 for engine)')
+ parser.add_argument('route', nargs='?', help='Route name, live if not specified')
+ parser.add_argument('--addrs', nargs='*', default=[], help='List of tx address to view (0x7e0 for engine)')
args = parser.parse_args()
addrs = [int(addr, base=16) if addr.startswith('0x') else int(addr) for addr in args.addrs]
diff --git a/selfdrive/debug/uiview.py b/selfdrive/debug/uiview.py
index 958ee72e04..8e75769a85 100755
--- a/selfdrive/debug/uiview.py
+++ b/selfdrive/debug/uiview.py
@@ -3,7 +3,7 @@ import time
from cereal import car, log, messaging
from openpilot.common.params import Params
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
from openpilot.system.hardware import HARDWARE
if __name__ == "__main__":
diff --git a/selfdrive/debug/vw_mqb_config.py b/selfdrive/debug/vw_mqb_config.py
index 64bc2bc638..6df1e57ce3 100755
--- a/selfdrive/debug/vw_mqb_config.py
+++ b/selfdrive/debug/vw_mqb_config.py
@@ -139,7 +139,7 @@ if __name__ == "__main__":
# so fib a little and say that same tester did the programming
current_date = date.today()
formatted_date = current_date.strftime('%y-%m-%d')
- year, month, day = [int(part) for part in formatted_date.split('-')]
+ year, month, day = (int(part) for part in formatted_date.split('-'))
prog_date = bytes([year, month, day])
uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.PROGRAMMING_DATE, prog_date)
tester_num = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.CALIBRATION_REPAIR_SHOP_CODE_OR_CALIBRATION_EQUIPMENT_SERIAL_NUMBER)
diff --git a/selfdrive/locationd/models/car_kf.py b/selfdrive/locationd/models/car_kf.py
index 1f3e447a19..87a93cdd0c 100755
--- a/selfdrive/locationd/models/car_kf.py
+++ b/selfdrive/locationd/models/car_kf.py
@@ -28,7 +28,7 @@ def _slice(n):
return s
-class States():
+class States:
# Vehicle model params
STIFFNESS = _slice(1) # [-]
STEER_RATIO = _slice(1) # [-]
diff --git a/selfdrive/locationd/models/live_kf.py b/selfdrive/locationd/models/live_kf.py
index c4933a6129..0cc3eca61d 100755
--- a/selfdrive/locationd/models/live_kf.py
+++ b/selfdrive/locationd/models/live_kf.py
@@ -20,7 +20,7 @@ def numpy2eigenstring(arr):
return f"(Eigen::VectorXd({len(arr)}) << {arr_str}).finished()"
-class States():
+class States:
ECEF_POS = slice(0, 3) # x, y and z in ECEF in meters
ECEF_ORIENTATION = slice(3, 7) # quat for pose of phone in ecef
ECEF_VELOCITY = slice(7, 10) # ecef velocity in m/s
@@ -39,7 +39,7 @@ class States():
ACC_BIAS_ERR = slice(18, 21)
-class LiveKalman():
+class LiveKalman:
name = 'live'
initial_x = np.array([3.88e6, -3.37e6, 3.76e6,
diff --git a/selfdrive/locationd/test/test_calibrationd.py b/selfdrive/locationd/test/test_calibrationd.py
index e2db094397..598d5d2d5f 100755
--- a/selfdrive/locationd/test/test_calibrationd.py
+++ b/selfdrive/locationd/test/test_calibrationd.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import random
-import unittest
import numpy as np
@@ -31,7 +30,7 @@ def process_messages(c, cam_odo_calib, cycles,
[0.0, 0.0, HEIGHT_INIT.item()],
[cam_odo_height_std, cam_odo_height_std, cam_odo_height_std])
-class TestCalibrationd(unittest.TestCase):
+class TestCalibrationd:
def test_read_saved_params(self):
msg = messaging.new_message('liveCalibration')
@@ -43,13 +42,13 @@ class TestCalibrationd(unittest.TestCase):
np.testing.assert_allclose(msg.liveCalibration.rpyCalib, c.rpy)
np.testing.assert_allclose(msg.liveCalibration.height, c.height)
- self.assertEqual(msg.liveCalibration.validBlocks, c.valid_blocks)
+ assert msg.liveCalibration.validBlocks == c.valid_blocks
def test_calibration_basics(self):
c = Calibrator(param_put=False)
process_messages(c, [0.0, 0.0, 0.0], BLOCK_SIZE * INPUTS_WANTED)
- self.assertEqual(c.valid_blocks, INPUTS_WANTED)
+ assert c.valid_blocks == INPUTS_WANTED
np.testing.assert_allclose(c.rpy, np.zeros(3))
np.testing.assert_allclose(c.height, HEIGHT_INIT)
c.reset()
@@ -59,7 +58,7 @@ class TestCalibrationd(unittest.TestCase):
c = Calibrator(param_put=False)
process_messages(c, [0.0, 0.0, 0.0], BLOCK_SIZE * INPUTS_WANTED, cam_odo_speed=MIN_SPEED_FILTER - 1)
process_messages(c, [0.0, 0.0, 0.0], BLOCK_SIZE * INPUTS_WANTED, carstate_speed=MIN_SPEED_FILTER - 1)
- self.assertEqual(c.valid_blocks, 0)
+ assert c.valid_blocks == 0
np.testing.assert_allclose(c.rpy, np.zeros(3))
np.testing.assert_allclose(c.height, HEIGHT_INIT)
@@ -67,7 +66,7 @@ class TestCalibrationd(unittest.TestCase):
def test_calibration_yaw_rate_reject(self):
c = Calibrator(param_put=False)
process_messages(c, [0.0, 0.0, 0.0], BLOCK_SIZE * INPUTS_WANTED, cam_odo_yr=MAX_YAW_RATE_FILTER)
- self.assertEqual(c.valid_blocks, 0)
+ assert c.valid_blocks == 0
np.testing.assert_allclose(c.rpy, np.zeros(3))
np.testing.assert_allclose(c.height, HEIGHT_INIT)
@@ -75,43 +74,40 @@ class TestCalibrationd(unittest.TestCase):
def test_calibration_speed_std_reject(self):
c = Calibrator(param_put=False)
process_messages(c, [0.0, 0.0, 0.0], BLOCK_SIZE * INPUTS_WANTED, cam_odo_speed_std=1e3)
- self.assertEqual(c.valid_blocks, INPUTS_NEEDED)
+ assert c.valid_blocks == INPUTS_NEEDED
np.testing.assert_allclose(c.rpy, np.zeros(3))
def test_calibration_speed_std_height_reject(self):
c = Calibrator(param_put=False)
process_messages(c, [0.0, 0.0, 0.0], BLOCK_SIZE * INPUTS_WANTED, cam_odo_height_std=1e3)
- self.assertEqual(c.valid_blocks, INPUTS_NEEDED)
+ assert c.valid_blocks == INPUTS_NEEDED
np.testing.assert_allclose(c.rpy, np.zeros(3))
def test_calibration_auto_reset(self):
c = Calibrator(param_put=False)
process_messages(c, [0.0, 0.0, 0.0], BLOCK_SIZE * INPUTS_NEEDED)
- self.assertEqual(c.valid_blocks, INPUTS_NEEDED)
+ assert c.valid_blocks == INPUTS_NEEDED
np.testing.assert_allclose(c.rpy, [0.0, 0.0, 0.0], atol=1e-3)
process_messages(c, [0.0, MAX_ALLOWED_PITCH_SPREAD*0.9, MAX_ALLOWED_YAW_SPREAD*0.9], BLOCK_SIZE + 10)
- self.assertEqual(c.valid_blocks, INPUTS_NEEDED + 1)
- self.assertEqual(c.cal_status, log.LiveCalibrationData.Status.calibrated)
+ assert c.valid_blocks == INPUTS_NEEDED + 1
+ assert c.cal_status == log.LiveCalibrationData.Status.calibrated
c = Calibrator(param_put=False)
process_messages(c, [0.0, 0.0, 0.0], BLOCK_SIZE * INPUTS_NEEDED)
- self.assertEqual(c.valid_blocks, INPUTS_NEEDED)
+ assert c.valid_blocks == INPUTS_NEEDED
np.testing.assert_allclose(c.rpy, [0.0, 0.0, 0.0])
process_messages(c, [0.0, MAX_ALLOWED_PITCH_SPREAD*1.1, 0.0], BLOCK_SIZE + 10)
- self.assertEqual(c.valid_blocks, 1)
- self.assertEqual(c.cal_status, log.LiveCalibrationData.Status.recalibrating)
+ assert c.valid_blocks == 1
+ assert c.cal_status == log.LiveCalibrationData.Status.recalibrating
np.testing.assert_allclose(c.rpy, [0.0, MAX_ALLOWED_PITCH_SPREAD*1.1, 0.0], atol=1e-2)
c = Calibrator(param_put=False)
process_messages(c, [0.0, 0.0, 0.0], BLOCK_SIZE * INPUTS_NEEDED)
- self.assertEqual(c.valid_blocks, INPUTS_NEEDED)
+ assert c.valid_blocks == INPUTS_NEEDED
np.testing.assert_allclose(c.rpy, [0.0, 0.0, 0.0])
process_messages(c, [0.0, 0.0, MAX_ALLOWED_YAW_SPREAD*1.1], BLOCK_SIZE + 10)
- self.assertEqual(c.valid_blocks, 1)
- self.assertEqual(c.cal_status, log.LiveCalibrationData.Status.recalibrating)
+ assert c.valid_blocks == 1
+ assert c.cal_status == log.LiveCalibrationData.Status.recalibrating
np.testing.assert_allclose(c.rpy, [0.0, 0.0, MAX_ALLOWED_YAW_SPREAD*1.1], atol=1e-2)
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/locationd/test/test_locationd.py b/selfdrive/locationd/test/test_locationd.py
index cd032dbaf0..f88f423cf1 100755
--- a/selfdrive/locationd/test/test_locationd.py
+++ b/selfdrive/locationd/test/test_locationd.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
+import pytest
import json
import random
-import unittest
import time
import capnp
@@ -10,16 +10,14 @@ from cereal.services import SERVICE_LIST
from openpilot.common.params import Params
from openpilot.common.transformations.coordinates import ecef2geodetic
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
-class TestLocationdProc(unittest.TestCase):
+class TestLocationdProc:
LLD_MSGS = ['gpsLocationExternal', 'cameraOdometry', 'carState', 'liveCalibration',
'accelerometer', 'gyroscope', 'magnetometer']
- def setUp(self):
- random.seed(123489234)
-
+ def setup_method(self):
self.pm = messaging.PubMaster(self.LLD_MSGS)
self.params = Params()
@@ -27,7 +25,7 @@ class TestLocationdProc(unittest.TestCase):
managed_processes['locationd'].prepare()
managed_processes['locationd'].start()
- def tearDown(self):
+ def teardown_method(self):
managed_processes['locationd'].stop()
def get_msg(self, name, t):
@@ -65,6 +63,7 @@ class TestLocationdProc(unittest.TestCase):
return msg
def test_params_gps(self):
+ random.seed(123489234)
self.params.remove('LastGPSPosition')
self.x = -2710700 + (random.random() * 1e5)
@@ -86,10 +85,6 @@ class TestLocationdProc(unittest.TestCase):
time.sleep(1) # wait for async params write
lastGPS = json.loads(self.params.get('LastGPSPosition'))
- self.assertAlmostEqual(lastGPS['latitude'], self.lat, places=3)
- self.assertAlmostEqual(lastGPS['longitude'], self.lon, places=3)
- self.assertAlmostEqual(lastGPS['altitude'], self.alt, places=3)
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert lastGPS['latitude'] == pytest.approx(self.lat, abs=0.001)
+ assert lastGPS['longitude'] == pytest.approx(self.lon, abs=0.001)
+ assert lastGPS['altitude'] == pytest.approx(self.alt, abs=0.001)
diff --git a/selfdrive/locationd/test/test_locationd_scenarios.py b/selfdrive/locationd/test/test_locationd_scenarios.py
index 3fdd47275f..be95c6fffb 100755
--- a/selfdrive/locationd/test/test_locationd_scenarios.py
+++ b/selfdrive/locationd/test/test_locationd_scenarios.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import pytest
-import unittest
import numpy as np
from collections import defaultdict
from enum import Enum
@@ -99,7 +98,7 @@ def run_scenarios(scenario, logs):
@pytest.mark.xdist_group("test_locationd_scenarios")
@pytest.mark.shared_download_cache
-class TestLocationdScenarios(unittest.TestCase):
+class TestLocationdScenarios:
"""
Test locationd with different scenarios. In all these scenarios, we expect the following:
- locationd kalman filter should never go unstable (we care mostly about yaw_rate, roll, gpsOK, inputsOK, sensorsOK)
@@ -107,7 +106,7 @@ class TestLocationdScenarios(unittest.TestCase):
"""
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
cls.logs = migrate_all(LogReader(TEST_ROUTE))
def test_base(self):
@@ -118,8 +117,8 @@ class TestLocationdScenarios(unittest.TestCase):
- roll: unchanged
"""
orig_data, replayed_data = run_scenarios(Scenario.BASE, self.logs)
- self.assertTrue(np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2)))
- self.assertTrue(np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5)))
+ assert np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2))
+ assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5))
def test_gps_off(self):
"""
@@ -130,9 +129,9 @@ class TestLocationdScenarios(unittest.TestCase):
- gpsOK: False
"""
orig_data, replayed_data = run_scenarios(Scenario.GPS_OFF, self.logs)
- self.assertTrue(np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2)))
- self.assertTrue(np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5)))
- self.assertTrue(np.all(replayed_data['gps_flag'] == 0.0))
+ assert np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2))
+ assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5))
+ assert np.all(replayed_data['gps_flag'] == 0.0)
def test_gps_off_midway(self):
"""
@@ -143,9 +142,9 @@ class TestLocationdScenarios(unittest.TestCase):
- gpsOK: True for the first half, False for the second half
"""
orig_data, replayed_data = run_scenarios(Scenario.GPS_OFF_MIDWAY, self.logs)
- self.assertTrue(np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2)))
- self.assertTrue(np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5)))
- self.assertTrue(np.diff(replayed_data['gps_flag'])[512] == -1.0)
+ assert np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2))
+ assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5))
+ assert np.diff(replayed_data['gps_flag'])[512] == -1.0
def test_gps_on_midway(self):
"""
@@ -156,9 +155,9 @@ class TestLocationdScenarios(unittest.TestCase):
- gpsOK: False for the first half, True for the second half
"""
orig_data, replayed_data = run_scenarios(Scenario.GPS_ON_MIDWAY, self.logs)
- self.assertTrue(np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2)))
- self.assertTrue(np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(1.5)))
- self.assertTrue(np.diff(replayed_data['gps_flag'])[505] == 1.0)
+ assert np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2))
+ assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(1.5))
+ assert np.diff(replayed_data['gps_flag'])[505] == 1.0
def test_gps_tunnel(self):
"""
@@ -169,10 +168,10 @@ class TestLocationdScenarios(unittest.TestCase):
- gpsOK: False for the middle section, True for the rest
"""
orig_data, replayed_data = run_scenarios(Scenario.GPS_TUNNEL, self.logs)
- self.assertTrue(np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2)))
- self.assertTrue(np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5)))
- self.assertTrue(np.diff(replayed_data['gps_flag'])[213] == -1.0)
- self.assertTrue(np.diff(replayed_data['gps_flag'])[805] == 1.0)
+ assert np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2))
+ assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5))
+ assert np.diff(replayed_data['gps_flag'])[213] == -1.0
+ assert np.diff(replayed_data['gps_flag'])[805] == 1.0
def test_gyro_off(self):
"""
@@ -183,9 +182,9 @@ class TestLocationdScenarios(unittest.TestCase):
- sensorsOK: False
"""
_, replayed_data = run_scenarios(Scenario.GYRO_OFF, self.logs)
- self.assertTrue(np.allclose(replayed_data['yaw_rate'], 0.0))
- self.assertTrue(np.allclose(replayed_data['roll'], 0.0))
- self.assertTrue(np.all(replayed_data['sensors_flag'] == 0.0))
+ assert np.allclose(replayed_data['yaw_rate'], 0.0)
+ assert np.allclose(replayed_data['roll'], 0.0)
+ assert np.all(replayed_data['sensors_flag'] == 0.0)
def test_gyro_spikes(self):
"""
@@ -196,10 +195,10 @@ class TestLocationdScenarios(unittest.TestCase):
- inputsOK: False for some time after the spike, True for the rest
"""
orig_data, replayed_data = run_scenarios(Scenario.GYRO_SPIKE_MIDWAY, self.logs)
- self.assertTrue(np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2)))
- self.assertTrue(np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5)))
- self.assertTrue(np.diff(replayed_data['inputs_flag'])[500] == -1.0)
- self.assertTrue(np.diff(replayed_data['inputs_flag'])[694] == 1.0)
+ assert np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2))
+ assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5))
+ assert np.diff(replayed_data['inputs_flag'])[500] == -1.0
+ assert np.diff(replayed_data['inputs_flag'])[694] == 1.0
def test_accel_off(self):
"""
@@ -210,9 +209,9 @@ class TestLocationdScenarios(unittest.TestCase):
- sensorsOK: False
"""
_, replayed_data = run_scenarios(Scenario.ACCEL_OFF, self.logs)
- self.assertTrue(np.allclose(replayed_data['yaw_rate'], 0.0))
- self.assertTrue(np.allclose(replayed_data['roll'], 0.0))
- self.assertTrue(np.all(replayed_data['sensors_flag'] == 0.0))
+ assert np.allclose(replayed_data['yaw_rate'], 0.0)
+ assert np.allclose(replayed_data['roll'], 0.0)
+ assert np.all(replayed_data['sensors_flag'] == 0.0)
def test_accel_spikes(self):
"""
@@ -221,9 +220,5 @@ class TestLocationdScenarios(unittest.TestCase):
Expected Result: Right now, the kalman filter is not robust to small spikes like it is to gyroscope spikes.
"""
orig_data, replayed_data = run_scenarios(Scenario.ACCEL_SPIKE_MIDWAY, self.logs)
- self.assertTrue(np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2)))
- self.assertTrue(np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5)))
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert np.allclose(orig_data['yaw_rate'], replayed_data['yaw_rate'], atol=np.radians(0.2))
+ assert np.allclose(orig_data['roll'], replayed_data['roll'], atol=np.radians(0.5))
diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript
index 2d15223d1e..deb84d5952 100644
--- a/selfdrive/modeld/SConscript
+++ b/selfdrive/modeld/SConscript
@@ -1,3 +1,5 @@
+import glob
+
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc', 'transformations')
lenv = env.Clone()
lenvCython = envCython.Clone()
@@ -50,11 +52,12 @@ lenvCython.Program('runners/runmodel_pyx.so', 'runners/runmodel_pyx.pyx', LIBS=c
lenvCython.Program('runners/snpemodel_pyx.so', 'runners/snpemodel_pyx.pyx', LIBS=[snpemodel_lib, snpe_lib, *cython_libs], FRAMEWORKS=frameworks, RPATH=snpe_rpath)
lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks)
+tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath)]
+
# Get model metadata
fn = File("models/supercombo").abspath
cmd = f'python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx'
-files = sum([lenv.Glob("#"+x) for x in open(File("#release/files_common").abspath).read().split("\n") if x.endswith("get_model_metadata.py")], [])
-lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"]+files, cmd)
+lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files, cmd)
# Build thneed model
if arch == "larch64" or GetOption('pc_thneed'):
@@ -64,7 +67,6 @@ if arch == "larch64" or GetOption('pc_thneed'):
tinygrad_opts += ["FLOAT16=1", "PYOPENCL_NO_CACHE=1"]
cmd = f"cd {Dir('#').abspath}/tinygrad_repo && " + ' '.join(tinygrad_opts) + f" python3 openpilot/compile2.py {fn}.onnx {fn}.thneed"
- tinygrad_files = sum([lenv.Glob("#"+x) for x in open(File("#release/files_common").abspath).read().split("\n") if x.startswith("tinygrad_repo/")], [])
lenv.Command(fn + ".thneed", [fn + ".onnx"] + tinygrad_files, cmd)
thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[gpucommon, common, 'zmq', 'OpenCL', 'dl'])
diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py
index 7972a0cd5a..8f417d7c7d 100755
--- a/selfdrive/modeld/modeld.py
+++ b/selfdrive/modeld/modeld.py
@@ -15,7 +15,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import config_realtime_process
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.common.transformations.model import get_warp_matrix
-from openpilot.selfdrive import sentry
+from openpilot.system import sentry
from openpilot.selfdrive.car.car_helpers import get_demo_car_params
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.modeld.runners import ModelRunner, Runtime
@@ -206,9 +206,8 @@ def main(demo=False):
continue
if abs(meta_main.timestamp_sof - meta_extra.timestamp_sof) > 10000000:
- cloudlog.error("frames out of sync! main: {} ({:.5f}), extra: {} ({:.5f})".format(
- meta_main.frame_id, meta_main.timestamp_sof / 1e9,
- meta_extra.frame_id, meta_extra.timestamp_sof / 1e9))
+ cloudlog.error(f"frames out of sync! main: {meta_main.frame_id} ({meta_main.timestamp_sof / 1e9:.5f}),\
+ extra: {meta_extra.frame_id} ({meta_extra.timestamp_sof / 1e9:.5f})")
else:
# Use single camera
diff --git a/selfdrive/modeld/models/commonmodel_pyx.pyx b/selfdrive/modeld/models/commonmodel_pyx.pyx
index e33d301aff..bc59b7a233 100644
--- a/selfdrive/modeld/models/commonmodel_pyx.pyx
+++ b/selfdrive/modeld/models/commonmodel_pyx.pyx
@@ -37,7 +37,11 @@ cdef class ModelFrame:
def prepare(self, VisionBuf buf, float[:] projection, CLMem output):
cdef mat3 cprojection
memcpy(cprojection.v, &projection[0], 9*sizeof(float))
- cdef float * data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, output.mem)
+ cdef float * data
+ if output is None:
+ data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, NULL)
+ else:
+ data = self.frame.prepare(buf.buf.buf_cl, buf.width, buf.height, buf.stride, buf.uv_offset, cprojection, output.mem)
if not data:
return None
return np.asarray( data)
diff --git a/selfdrive/modeld/models/dmonitoring_model.current b/selfdrive/modeld/models/dmonitoring_model.current
index ed495df3a8..f935ab06b3 100644
--- a/selfdrive/modeld/models/dmonitoring_model.current
+++ b/selfdrive/modeld/models/dmonitoring_model.current
@@ -1,2 +1,2 @@
-60abed33-9f25-4e34-9937-aaf918d41dfc
-50887963963a3022e85ac0c94b0801aef955a608
\ No newline at end of file
+5ec97a39-0095-4cea-adfa-6d72b1966cc1
+26cac7a9757a27c783a365403040a1bd27ccdaea
\ No newline at end of file
diff --git a/selfdrive/modeld/models/dmonitoring_model.onnx b/selfdrive/modeld/models/dmonitoring_model.onnx
index 3e94e7d36a..dd3a6aba2e 100644
--- a/selfdrive/modeld/models/dmonitoring_model.onnx
+++ b/selfdrive/modeld/models/dmonitoring_model.onnx
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9a667cbde6eca86c5b653f57853e3d33b9a875bceb557e193d1bef78c2df9c37
-size 16132779
+oid sha256:3dd3982940d823c4fbb0429b733a0b78b0688d7d67aa76ff7b754a3e2f3d8683
+size 16132780
diff --git a/selfdrive/modeld/models/dmonitoring_model_q.dlc b/selfdrive/modeld/models/dmonitoring_model_q.dlc
index 041949ad94..fc285954d4 100644
--- a/selfdrive/modeld/models/dmonitoring_model_q.dlc
+++ b/selfdrive/modeld/models/dmonitoring_model_q.dlc
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f5729c457dd47f6c35e2a818eae14113526fd0bfac6417a809cbaf9d38697d9b
-size 4488413
+oid sha256:7c26f13816b143f5bb29ac2980f8557bd5687a75729e4d895313fb9a5a1f0f46
+size 4488449
diff --git a/selfdrive/modeld/tests/test_modeld.py b/selfdrive/modeld/tests/test_modeld.py
index 67c6f71038..5cbdbc2bb4 100755
--- a/selfdrive/modeld/tests/test_modeld.py
+++ b/selfdrive/modeld/tests/test_modeld.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
-import unittest
import numpy as np
import random
@@ -8,7 +7,7 @@ from cereal.visionipc import VisionIpcServer, VisionStreamType
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.car_helpers import write_car_param
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state
CAM = DEVICE_CAMERAS[("tici", "ar0231")].fcam
@@ -16,9 +15,9 @@ IMG = np.zeros(int(CAM.width*CAM.height*(3/2)), dtype=np.uint8)
IMG_BYTES = IMG.flatten().tobytes()
-class TestModeld(unittest.TestCase):
+class TestModeld:
- def setUp(self):
+ def setup_method(self):
self.vipc_server = VisionIpcServer("camerad")
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 40, False, CAM.width, CAM.height)
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_DRIVER, 40, False, CAM.width, CAM.height)
@@ -32,7 +31,7 @@ class TestModeld(unittest.TestCase):
managed_processes['modeld'].start()
self.pm.wait_for_readers_to_update("roadCameraState", 10)
- def tearDown(self):
+ def teardown_method(self):
managed_processes['modeld'].stop()
del self.vipc_server
@@ -65,15 +64,15 @@ class TestModeld(unittest.TestCase):
self._wait()
mdl = self.sm['modelV2']
- self.assertEqual(mdl.frameId, n)
- self.assertEqual(mdl.frameIdExtra, n)
- self.assertEqual(mdl.timestampEof, cs.timestampEof)
- self.assertEqual(mdl.frameAge, 0)
- self.assertEqual(mdl.frameDropPerc, 0)
+ assert mdl.frameId == n
+ assert mdl.frameIdExtra == n
+ assert mdl.timestampEof == cs.timestampEof
+ assert mdl.frameAge == 0
+ assert mdl.frameDropPerc == 0
odo = self.sm['cameraOdometry']
- self.assertEqual(odo.frameId, n)
- self.assertEqual(odo.timestampEof, cs.timestampEof)
+ assert odo.frameId == n
+ assert odo.timestampEof == cs.timestampEof
def test_dropped_frames(self):
"""
@@ -95,13 +94,9 @@ class TestModeld(unittest.TestCase):
mdl = self.sm['modelV2']
odo = self.sm['cameraOdometry']
- self.assertEqual(mdl.frameId, frame_id)
- self.assertEqual(mdl.frameIdExtra, frame_id)
- self.assertEqual(odo.frameId, frame_id)
+ assert mdl.frameId == frame_id
+ assert mdl.frameIdExtra == frame_id
+ assert odo.frameId == frame_id
if n != frame_id:
- self.assertFalse(self.sm.updated['modelV2'])
- self.assertFalse(self.sm.updated['cameraOdometry'])
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert not self.sm.updated['modelV2']
+ assert not self.sm.updated['cameraOdometry']
diff --git a/selfdrive/modeld/tests/timing/benchmark.py b/selfdrive/modeld/tests/timing/benchmark.py
index 4ab0afacbc..c629ec2fff 100755
--- a/selfdrive/modeld/tests/timing/benchmark.py
+++ b/selfdrive/modeld/tests/timing/benchmark.py
@@ -6,7 +6,7 @@ import time
import numpy as np
import cereal.messaging as messaging
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
N = int(os.getenv("N", "5"))
diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py
index 0bd95172d3..80df13f0cb 100755
--- a/selfdrive/monitoring/dmonitoringd.py
+++ b/selfdrive/monitoring/dmonitoringd.py
@@ -2,12 +2,9 @@
import gc
import cereal.messaging as messaging
-from cereal import car
from openpilot.common.params import Params
from openpilot.common.realtime import set_realtime_priority
-from openpilot.selfdrive.controls.lib.events import Events
-from openpilot.selfdrive.monitoring.driver_monitor import DriverStatus
-from openpilot.selfdrive.monitoring.hands_on_wheel_monitor import HandsOnWheelStatus
+from openpilot.selfdrive.monitoring.helpers import DriverMonitoring
def dmonitoringd_thread():
@@ -18,90 +15,36 @@ def dmonitoringd_thread():
pm = messaging.PubMaster(['driverMonitoringState', 'driverMonitoringStateSP'])
sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'controlsState', 'modelV2'], poll='driverStateV2')
- driver_status = DriverStatus(rhd_saved=params.get_bool("IsRhdDetected"), always_on=params.get_bool("AlwaysOnDM"))
- hands_on_wheel_status = HandsOnWheelStatus()
-
- v_cruise_last = 0
- driver_engaged = False
- steering_wheel_engaged = False
- hands_on_wheel_monitoring_enabled = params.get_bool("HandsOnWheelMonitoring")
+ DM = DriverMonitoring(rhd_saved=params.get_bool("IsRhdDetected"), always_on=params.get_bool("AlwaysOnDM"), hands_on_wheel_monitoring=params.get_bool("HandsOnWheelMonitoring"))
# 20Hz <- dmonitoringmodeld
while True:
sm.update()
if not sm.updated['driverStateV2']:
+ # iterate when model has new output
continue
- # Get interaction
- if sm.updated['carState']:
- v_cruise = sm['carState'].cruiseState.speed
- steering_wheel_engaged = len(sm['carState'].buttonEvents) > 0 or \
- v_cruise != v_cruise_last or \
- sm['carState'].steeringPressed
- driver_engaged = steering_wheel_engaged or sm['carState'].gasPressed
- # Update events and state from hands on wheel monitoring status when steering wheel in engaged
- if steering_wheel_engaged and hands_on_wheel_monitoring_enabled:
- hands_on_wheel_status.update(Events(), True, sm['controlsState'].enabled, sm['carState'].vEgo)
- v_cruise_last = v_cruise
+ valid = sm.all_checks()
+ if valid:
+ DM.run_step(sm)
- if sm.updated['modelV2']:
- driver_status.set_policy(sm['modelV2'], sm['carState'].vEgo)
-
- # Get data from dmonitoringmodeld
- events = Events()
-
- if sm.all_checks() and len(sm['liveCalibration'].rpyCalib):
- driver_status.update_states(sm['driverStateV2'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled)
-
- # Block engaging after max number of distrations or when alert active
- if driver_status.terminal_alert_cnt >= driver_status.settings._MAX_TERMINAL_ALERTS or \
- driver_status.terminal_time >= driver_status.settings._MAX_TERMINAL_DURATION or \
- driver_status.always_on and driver_status.awareness <= driver_status.threshold_prompt:
- events.add(car.CarEvent.EventName.tooDistracted)
-
- # Update events from driver state
- driver_status.update_events(events, driver_engaged, sm['controlsState'].enabled,
- sm['carState'].standstill, sm['carState'].gearShifter in [car.CarState.GearShifter.reverse, car.CarState.GearShifter.park], sm['carState'].vEgo)
- # Update events and state from hands on wheel monitoring status
- if hands_on_wheel_monitoring_enabled:
- hands_on_wheel_status.update(events, steering_wheel_engaged, sm['controlsState'].enabled, sm['carState'].vEgo)
-
- # build driverMonitoringState packet
- dat = messaging.new_message('driverMonitoringState', valid=sm.all_checks())
- dat.driverMonitoringState = {
- "events": events.to_msg(),
- "faceDetected": driver_status.face_detected,
- "isDistracted": driver_status.driver_distracted,
- "distractedType": sum(driver_status.distracted_types),
- "awarenessStatus": driver_status.awareness,
- "posePitchOffset": driver_status.pose.pitch_offseter.filtered_stat.mean(),
- "posePitchValidCount": driver_status.pose.pitch_offseter.filtered_stat.n,
- "poseYawOffset": driver_status.pose.yaw_offseter.filtered_stat.mean(),
- "poseYawValidCount": driver_status.pose.yaw_offseter.filtered_stat.n,
- "stepChange": driver_status.step_change,
- "awarenessActive": driver_status.awareness_active,
- "awarenessPassive": driver_status.awareness_passive,
- "isLowStd": driver_status.pose.low_std,
- "hiStdCount": driver_status.hi_stds,
- "isActiveMode": driver_status.active_monitoring_mode,
- "isRHD": driver_status.wheel_on_right,
- }
+ # publish
+ dat = DM.get_state_packet(valid=valid)
pm.send('driverMonitoringState', dat)
- if sm['driverStateV2'].frameId % 40 == 1:
- driver_status.always_on = params.get_bool("AlwaysOnDM")
-
- sp_dat = messaging.new_message('driverMonitoringStateSP')
- sp_dat.driverMonitoringStateSP = {
- "handsOnWheelState": hands_on_wheel_status.hands_on_wheel_state,
- }
+ sp_dat = DM.get_sp_state_packet(valid=valid)
pm.send('driverMonitoringStateSP', sp_dat)
+ # load live always-on toggle
+ if sm['driverStateV2'].frameId % 40 == 1:
+ DM.always_on = params.get_bool("AlwaysOnDM")
+ DM.hands_on_wheel_monitoring = params.get_bool("HandsOnWheelMonitoring")
+
# save rhd virtual toggle every 5 mins
if (sm['driverStateV2'].frameId % 6000 == 0 and
- driver_status.wheelpos_learner.filtered_stat.n > driver_status.settings._WHEELPOS_FILTER_MIN_COUNT and
- driver_status.wheel_on_right == (driver_status.wheelpos_learner.filtered_stat.M > driver_status.settings._WHEELPOS_THRESHOLD)):
- params.put_bool_nonblocking("IsRhdDetected", driver_status.wheel_on_right)
+ DM.wheelpos_learner.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
+ DM.wheel_on_right == (DM.wheelpos_learner.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
+ params.put_bool_nonblocking("IsRhdDetected", DM.wheel_on_right)
def main():
dmonitoringd_thread()
diff --git a/selfdrive/monitoring/hands_on_wheel_monitor.py b/selfdrive/monitoring/hands_on_wheel_monitor.py
index 713d3a8d89..e281c04909 100644
--- a/selfdrive/monitoring/hands_on_wheel_monitor.py
+++ b/selfdrive/monitoring/hands_on_wheel_monitor.py
@@ -8,7 +8,7 @@ _PRE_ALERT_THRESHOLD = 150 # 15s
_PROMPT_ALERT_THRESHOLD = 300 # 30s
_TERMINAL_ALERT_THRESHOLD = 600 # 60s
-_MIN_MONITORING_SPEED = 10 * CV.KPH_TO_MS # No monitoring underd 10kph
+_MIN_MONITORING_SPEED = 10 * CV.KPH_TO_MS # No monitoring under 10kph
class HandsOnWheelStatus():
@@ -16,8 +16,11 @@ class HandsOnWheelStatus():
self.hands_on_wheel_state = HandsOnWheelState.none
self.hands_off_wheel_cnt = 0
- def update(self, events, steering_wheel_engaged, ctrl_active, v_ego):
- if v_ego < _MIN_MONITORING_SPEED or not ctrl_active:
+ def update(self, events, steering_wheel_engaged, ctrl_active, v_ego, always_on, hands_on_wheel_monitoring):
+ if v_ego < _MIN_MONITORING_SPEED or \
+ (not always_on and not ctrl_active) or \
+ (always_on and not ctrl_active and self.hands_on_wheel_state == HandsOnWheelState.terminal) or \
+ not hands_on_wheel_monitoring:
self.hands_on_wheel_state = HandsOnWheelState.none
self.hands_off_wheel_cnt = 0
return
diff --git a/selfdrive/monitoring/driver_monitor.py b/selfdrive/monitoring/helpers.py
similarity index 78%
rename from selfdrive/monitoring/driver_monitor.py
rename to selfdrive/monitoring/helpers.py
index 7db0bd6c9f..ec41b7855b 100644
--- a/selfdrive/monitoring/driver_monitor.py
+++ b/selfdrive/monitoring/helpers.py
@@ -1,6 +1,9 @@
from math import atan2
from cereal import car
+import cereal.messaging as messaging
+from openpilot.selfdrive.controls.lib.events import Events
+from openpilot.selfdrive.monitoring.hands_on_wheel_monitor import HandsOnWheelStatus
from openpilot.common.numpy_fast import interp
from openpilot.common.realtime import DT_DMON
from openpilot.common.filter_simple import FirstOrderFilter
@@ -15,7 +18,7 @@ EventName = car.CarEvent.EventName
# We recommend that you do not change these numbers from the defaults.
# ******************************************************************************************
-class DRIVER_MONITOR_SETTINGS():
+class DRIVER_MONITOR_SETTINGS:
def __init__(self):
self._DT_DMON = DT_DMON
# ref (page15-16): https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:42018X1947&rid=2
@@ -31,8 +34,8 @@ class DRIVER_MONITOR_SETTINGS():
self._SG_THRESHOLD = 0.9
self._BLINK_THRESHOLD = 0.865
- self._EE_THRESH11 = 0.241
- self._EE_THRESH12 = 4.7
+ self._EE_THRESH11 = 0.25
+ self._EE_THRESH12 = 7.5
self._EE_MAX_OFFSET1 = 0.06
self._EE_MIN_OFFSET1 = 0.025
self._EE_THRESH21 = 0.01
@@ -71,19 +74,38 @@ class DRIVER_MONITOR_SETTINGS():
self._MAX_TERMINAL_ALERTS = 3 # not allowed to engage after 3 terminal alerts
self._MAX_TERMINAL_DURATION = int(30 / self._DT_DMON) # not allowed to engage after 30s of terminal alerts
+class DistractedType:
+ NOT_DISTRACTED = 0
+ DISTRACTED_POSE = 1 << 0
+ DISTRACTED_BLINK = 1 << 1
+ DISTRACTED_E2E = 1 << 2
+
+class DriverPose:
+ def __init__(self, max_trackable):
+ self.yaw = 0.
+ self.pitch = 0.
+ self.roll = 0.
+ self.yaw_std = 0.
+ self.pitch_std = 0.
+ self.roll_std = 0.
+ self.pitch_offseter = RunningStatFilter(max_trackable=max_trackable)
+ self.yaw_offseter = RunningStatFilter(max_trackable=max_trackable)
+ self.calibrated = False
+ self.low_std = True
+ self.cfactor_pitch = 1.
+ self.cfactor_yaw = 1.
+
+class DriverBlink:
+ def __init__(self):
+ self.left = 0.
+ self.right = 0.
+
-# TODO: get these live
# model output refers to center of undistorted+leveled image
EFL = 598.0 # focal length in K
cam = DEVICE_CAMERAS[("tici", "ar0231")] # corrected image has same size as raw
W, H = (cam.dcam.width, cam.dcam.height) # corrected image has same size as raw
-class DistractedType:
- NOT_DISTRACTED = 0
- DISTRACTED_POSE = 1
- DISTRACTED_BLINK = 2
- DISTRACTED_E2E = 4
-
def face_orientation_from_net(angles_desc, pos_desc, rpy_calib):
# the output of these angles are in device frame
# so from driver's perspective, pitch is up and yaw is right
@@ -102,27 +124,9 @@ def face_orientation_from_net(angles_desc, pos_desc, rpy_calib):
yaw -= rpy_calib[2]
return roll_net, pitch, yaw
-class DriverPose():
- def __init__(self, max_trackable):
- self.yaw = 0.
- self.pitch = 0.
- self.roll = 0.
- self.yaw_std = 0.
- self.pitch_std = 0.
- self.roll_std = 0.
- self.pitch_offseter = RunningStatFilter(max_trackable=max_trackable)
- self.yaw_offseter = RunningStatFilter(max_trackable=max_trackable)
- self.low_std = True
- self.cfactor_pitch = 1.
- self.cfactor_yaw = 1.
-class DriverBlink():
- def __init__(self):
- self.left_blink = 0.
- self.right_blink = 0.
-
-class DriverStatus():
- def __init__(self, rhd_saved=False, settings=None, always_on=False):
+class DriverMonitoring:
+ def __init__(self, rhd_saved=False, settings=None, always_on=False, hands_on_wheel_monitoring=False):
if settings is None:
settings = DRIVER_MONITOR_SETTINGS()
# init policy settings
@@ -131,7 +135,6 @@ class DriverStatus():
# init driver status
self.wheelpos_learner = RunningStatFilter()
self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT)
- self.pose_calibrated = False
self.blink = DriverBlink()
self.eev1 = 0.
self.eev2 = 1.
@@ -141,9 +144,6 @@ class DriverStatus():
self.ee2_calibrated = False
self.always_on = always_on
- self.awareness = 1.
- self.awareness_active = 1.
- self.awareness_passive = 1.
self.distracted_types = []
self.driver_distracted = False
self.driver_distraction_filter = FirstOrderFilter(0., self.settings._DISTRACTED_FILTER_TS, self.settings._DT_DMON)
@@ -160,13 +160,21 @@ class DriverStatus():
self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
+ self._reset_awareness()
self._set_timers(active_monitoring=True)
+ self._reset_events()
+
+ self.hands_on_wheel_status = HandsOnWheelStatus()
+ self.hands_on_wheel_monitoring = hands_on_wheel_monitoring
def _reset_awareness(self):
self.awareness = 1.
self.awareness_active = 1.
self.awareness_passive = 1.
+ def _reset_events(self):
+ self.current_events = Events()
+
def _set_timers(self, active_monitoring):
if self.active_monitoring_mode and self.awareness <= self.threshold_prompt:
if active_monitoring:
@@ -197,41 +205,7 @@ class DriverStatus():
self.step_change = self.settings._DT_DMON / self.settings._AWARENESS_TIME
self.active_monitoring_mode = False
- def _get_distracted_types(self):
- distracted_types = []
-
- if not self.pose_calibrated:
- pitch_error = self.pose.pitch - self.settings._PITCH_NATURAL_OFFSET
- yaw_error = self.pose.yaw - self.settings._YAW_NATURAL_OFFSET
- else:
- pitch_error = self.pose.pitch - min(max(self.pose.pitch_offseter.filtered_stat.mean(),
- self.settings._PITCH_MIN_OFFSET), self.settings._PITCH_MAX_OFFSET)
- yaw_error = self.pose.yaw - min(max(self.pose.yaw_offseter.filtered_stat.mean(),
- self.settings._YAW_MIN_OFFSET), self.settings._YAW_MAX_OFFSET)
- pitch_error = 0 if pitch_error > 0 else abs(pitch_error) # no positive pitch limit
- yaw_error = abs(yaw_error)
- if pitch_error > (self.settings._POSE_PITCH_THRESHOLD*self.pose.cfactor_pitch if self.pose_calibrated else self.settings._PITCH_NATURAL_THRESHOLD) or \
- yaw_error > self.settings._POSE_YAW_THRESHOLD*self.pose.cfactor_yaw:
- distracted_types.append(DistractedType.DISTRACTED_POSE)
-
- if (self.blink.left_blink + self.blink.right_blink)*0.5 > self.settings._BLINK_THRESHOLD:
- distracted_types.append(DistractedType.DISTRACTED_BLINK)
-
- if self.ee1_calibrated:
- ee1_dist = self.eev1 > max(min(self.ee1_offseter.filtered_stat.M, self.settings._EE_MAX_OFFSET1), self.settings._EE_MIN_OFFSET1) \
- * self.settings._EE_THRESH12
- else:
- ee1_dist = self.eev1 > self.settings._EE_THRESH11
- # if self.ee2_calibrated:
- # ee2_dist = self.eev2 < self.ee2_offseter.filtered_stat.M * self.settings._EE_THRESH22
- # else:
- # ee2_dist = self.eev2 < self.settings._EE_THRESH21
- if ee1_dist:
- distracted_types.append(DistractedType.DISTRACTED_E2E)
-
- return distracted_types
-
- def set_policy(self, model_data, car_speed):
+ def _set_policy(self, model_data, car_speed):
bp = model_data.meta.disengagePredictions.brakeDisengageProbs[0] # brake disengage prob in next 2s
k1 = max(-0.00156*((car_speed-16)**2)+0.6, 0.2)
bp_normal = max(min(bp / k1, 0.5),0)
@@ -242,7 +216,37 @@ class DriverStatus():
[self.settings._POSE_YAW_THRESHOLD_SLACK,
self.settings._POSE_YAW_THRESHOLD_STRICT]) / self.settings._POSE_YAW_THRESHOLD
- def update_states(self, driver_state, cal_rpy, car_speed, op_engaged):
+ def _get_distracted_types(self):
+ distracted_types = []
+
+ if not self.pose.calibrated:
+ pitch_error = self.pose.pitch - self.settings._PITCH_NATURAL_OFFSET
+ yaw_error = self.pose.yaw - self.settings._YAW_NATURAL_OFFSET
+ else:
+ pitch_error = self.pose.pitch - min(max(self.pose.pitch_offseter.filtered_stat.mean(),
+ self.settings._PITCH_MIN_OFFSET), self.settings._PITCH_MAX_OFFSET)
+ yaw_error = self.pose.yaw - min(max(self.pose.yaw_offseter.filtered_stat.mean(),
+ self.settings._YAW_MIN_OFFSET), self.settings._YAW_MAX_OFFSET)
+ pitch_error = 0 if pitch_error > 0 else abs(pitch_error) # no positive pitch limit
+ yaw_error = abs(yaw_error)
+ if pitch_error > (self.settings._POSE_PITCH_THRESHOLD*self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD) or \
+ yaw_error > self.settings._POSE_YAW_THRESHOLD*self.pose.cfactor_yaw:
+ distracted_types.append(DistractedType.DISTRACTED_POSE)
+
+ if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
+ distracted_types.append(DistractedType.DISTRACTED_BLINK)
+
+ if self.ee1_calibrated:
+ ee1_dist = self.eev1 > max(min(self.ee1_offseter.filtered_stat.M, self.settings._EE_MAX_OFFSET1), self.settings._EE_MIN_OFFSET1) \
+ * self.settings._EE_THRESH12
+ else:
+ ee1_dist = self.eev1 > self.settings._EE_THRESH11
+ if ee1_dist:
+ distracted_types.append(DistractedType.DISTRACTED_E2E)
+
+ return distracted_types
+
+ def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged):
rhd_pred = driver_state.wheelOnRightProb
# calibrates only when there's movement and either face detected
if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or
@@ -270,9 +274,9 @@ class DriverStatus():
self.pose.yaw_std = driver_data.faceOrientationStd[1]
model_std_max = max(self.pose.pitch_std, self.pose.yaw_std)
self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD
- self.blink.left_blink = driver_data.leftBlinkProb * (driver_data.leftEyeProb > self.settings._EYE_THRESHOLD) \
+ self.blink.left = driver_data.leftBlinkProb * (driver_data.leftEyeProb > self.settings._EYE_THRESHOLD) \
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
- self.blink.right_blink = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
+ self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
self.eev1 = driver_data.notReadyProb[0]
self.eev2 = driver_data.readyProb[0]
@@ -291,7 +295,7 @@ class DriverStatus():
self.ee1_offseter.push_and_update(self.eev1)
self.ee2_offseter.push_and_update(self.eev2)
- self.pose_calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
+ self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
self.ee1_calibrated = self.ee1_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
self.ee2_calibrated = self.ee2_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
@@ -303,11 +307,22 @@ class DriverStatus():
elif self.face_detected and self.pose.low_std:
self.hi_stds = 0
- def update_events(self, events, driver_engaged, ctrl_active, standstill, wrong_gear, car_speed):
+ def _update_events(self, driver_engaged, op_engaged, standstill, wrong_gear, car_speed, steering_wheel_engaged):
+ self._reset_events()
+ # Block engaging after max number of distrations or when alert active
+ if self.terminal_alert_cnt >= self.settings._MAX_TERMINAL_ALERTS or \
+ self.terminal_time >= self.settings._MAX_TERMINAL_DURATION or \
+ self.always_on and self.awareness <= self.threshold_prompt:
+ self.current_events.add(EventName.tooDistracted)
+
always_on_valid = self.always_on and not wrong_gear
+
+ # Update events and state from hands on wheel monitoring status
+ self.hands_on_wheel_status.update(self.current_events, steering_wheel_engaged, op_engaged, car_speed, always_on_valid, self.hands_on_wheel_monitoring)
+
if (driver_engaged and self.awareness > 0 and not self.active_monitoring_mode) or \
- (not always_on_valid and not ctrl_active) or \
- (always_on_valid and not ctrl_active and self.awareness <= 0):
+ (not always_on_valid and not op_engaged) or \
+ (always_on_valid and not op_engaged and self.awareness <= 0):
# always reset on disengage with normal mode; disengage resets only on red if always on
self._reset_awareness()
return
@@ -331,8 +346,8 @@ class DriverStatus():
_reaching_audible = self.awareness - self.step_change <= self.threshold_prompt
_reaching_terminal = self.awareness - self.step_change <= 0
standstill_exemption = standstill and _reaching_audible
- always_on_red_exemption = always_on_valid and not ctrl_active and _reaching_terminal
- always_on_lowspeed_exemption = always_on_valid and not ctrl_active and car_speed < self.settings._ALWAYS_ON_ALERT_MIN_SPEED and _reaching_audible
+ always_on_red_exemption = always_on_valid and not op_engaged and _reaching_terminal
+ always_on_lowspeed_exemption = always_on_valid and not op_engaged and car_speed < self.settings._ALWAYS_ON_ALERT_MIN_SPEED and _reaching_audible
certainly_distracted = self.driver_distraction_filter.x > 0.63 and self.driver_distracted and self.face_detected
maybe_distracted = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME or not self.face_detected
@@ -358,4 +373,60 @@ class DriverStatus():
alert = EventName.preDriverDistracted if self.active_monitoring_mode else EventName.preDriverUnresponsive
if alert is not None:
- events.add(alert)
+ self.current_events.add(alert)
+
+
+ def get_state_packet(self, valid=True):
+ # build driverMonitoringState packet
+ dat = messaging.new_message('driverMonitoringState', valid=valid)
+ dat.driverMonitoringState = {
+ "events": self.current_events.to_msg(),
+ "faceDetected": self.face_detected,
+ "isDistracted": self.driver_distracted,
+ "distractedType": sum(self.distracted_types),
+ "awarenessStatus": self.awareness,
+ "posePitchOffset": self.pose.pitch_offseter.filtered_stat.mean(),
+ "posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n,
+ "poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(),
+ "poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n,
+ "stepChange": self.step_change,
+ "awarenessActive": self.awareness_active,
+ "awarenessPassive": self.awareness_passive,
+ "isLowStd": self.pose.low_std,
+ "hiStdCount": self.hi_stds,
+ "isActiveMode": self.active_monitoring_mode,
+ "isRHD": self.wheel_on_right,
+ }
+ return dat
+
+ def get_sp_state_packet(self, valid=True):
+ dat = messaging.new_message('driverMonitoringStateSP', valid=valid)
+ dat.driverMonitoringStateSP = {
+ "handsOnWheelState": self.hands_on_wheel_status.hands_on_wheel_state,
+ }
+ return dat
+
+ def run_step(self, sm):
+ # Set strictness
+ self._set_policy(
+ model_data=sm['modelV2'],
+ car_speed=sm['carState'].vEgo
+ )
+
+ # Parse data from dmonitoringmodeld
+ self._update_states(
+ driver_state=sm['driverStateV2'],
+ cal_rpy=sm['liveCalibration'].rpyCalib,
+ car_speed=sm['carState'].vEgo,
+ op_engaged=sm['controlsState'].enabled
+ )
+
+ # Update distraction events
+ self._update_events(
+ driver_engaged=sm['carState'].steeringPressed or sm['carState'].gasPressed,
+ op_engaged=sm['controlsState'].enabled,
+ standstill=sm['carState'].standstill,
+ wrong_gear=sm['carState'].gearShifter in [car.CarState.GearShifter.reverse, car.CarState.GearShifter.park],
+ car_speed=sm['carState'].vEgo,
+ steering_wheel_engaged=sm['carState'].steeringPressed
+ )
diff --git a/selfdrive/monitoring/test_monitoring.py b/selfdrive/monitoring/test_monitoring.py
index 50b2746e2d..a960a379e2 100755
--- a/selfdrive/monitoring/test_monitoring.py
+++ b/selfdrive/monitoring/test_monitoring.py
@@ -1,11 +1,9 @@
#!/usr/bin/env python3
-import unittest
import numpy as np
from cereal import car, log
from openpilot.common.realtime import DT_DMON
-from openpilot.selfdrive.controls.lib.events import Events
-from openpilot.selfdrive.monitoring.driver_monitor import DriverStatus, DRIVER_MONITOR_SETTINGS
+from openpilot.selfdrive.monitoring.helpers import DriverMonitoring, DRIVER_MONITOR_SETTINGS
EventName = car.CarEvent.EventName
dm_settings = DRIVER_MONITOR_SETTINGS()
@@ -52,24 +50,22 @@ always_distracted = [msg_DISTRACTED] * int(TEST_TIMESPAN / DT_DMON)
always_true = [True] * int(TEST_TIMESPAN / DT_DMON)
always_false = [False] * int(TEST_TIMESPAN / DT_DMON)
-# TODO: this only tests DriverStatus
-class TestMonitoring(unittest.TestCase):
+class TestMonitoring:
def _run_seq(self, msgs, interaction, engaged, standstill):
- DS = DriverStatus()
+ DM = DriverMonitoring()
events = []
for idx in range(len(msgs)):
- e = Events()
- DS.update_states(msgs[idx], [0, 0, 0], 0, engaged[idx])
+ DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx])
# cal_rpy and car_speed don't matter here
# evaluate events at 10Hz for tests
- DS.update_events(e, interaction[idx], engaged[idx], standstill[idx], 0, 0)
- events.append(e)
+ DM._update_events(interaction[idx], engaged[idx], standstill[idx], 0, 0)
+ events.append(DM.current_events)
assert len(events) == len(msgs), f"got {len(events)} for {len(msgs)} driverState input msgs"
- return events, DS
+ return events, DM
def _assert_no_events(self, events):
- self.assertTrue(all(not len(e) for e in events))
+ assert all(not len(e) for e in events)
# engaged, driver is attentive all the time
def test_fully_aware_driver(self):
@@ -79,27 +75,27 @@ class TestMonitoring(unittest.TestCase):
# engaged, driver is distracted and does nothing
def test_fully_distracted_driver(self):
events, d_status = self._run_seq(always_distracted, always_false, always_true, always_false)
- self.assertEqual(len(events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL)/2/DT_DMON)]), 0)
- self.assertEqual(events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL +
- ((d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL-d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0],
- EventName.preDriverDistracted)
- self.assertEqual(events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL +
- ((d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0], EventName.promptDriverDistracted)
- self.assertEqual(events[int((d_status.settings._DISTRACTED_TIME +
- ((TEST_TIMESPAN-10-d_status.settings._DISTRACTED_TIME)/2))/DT_DMON)].names[0], EventName.driverDistracted)
- self.assertIs(type(d_status.awareness), float)
+ assert len(events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL)/2/DT_DMON)]) == 0
+ assert events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL + \
+ ((d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL-d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == \
+ EventName.preDriverDistracted
+ assert events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL + \
+ ((d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == EventName.promptDriverDistracted
+ assert events[int((d_status.settings._DISTRACTED_TIME + \
+ ((TEST_TIMESPAN-10-d_status.settings._DISTRACTED_TIME)/2))/DT_DMON)].names[0] == EventName.driverDistracted
+ assert isinstance(d_status.awareness, float)
# engaged, no face detected the whole time, no action
def test_fully_invisible_driver(self):
events, d_status = self._run_seq(always_no_face, always_false, always_true, always_false)
- self.assertTrue(len(events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL)/2/DT_DMON)]) == 0)
- self.assertEqual(events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL +
- ((d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL-d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0],
- EventName.preDriverUnresponsive)
- self.assertEqual(events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL +
- ((d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0], EventName.promptDriverUnresponsive)
- self.assertEqual(events[int((d_status.settings._AWARENESS_TIME +
- ((TEST_TIMESPAN-10-d_status.settings._AWARENESS_TIME)/2))/DT_DMON)].names[0], EventName.driverUnresponsive)
+ assert len(events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL)/2/DT_DMON)]) == 0
+ assert events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL + \
+ ((d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL-d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == \
+ EventName.preDriverUnresponsive
+ assert events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL + \
+ ((d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == EventName.promptDriverUnresponsive
+ assert events[int((d_status.settings._AWARENESS_TIME + \
+ ((TEST_TIMESPAN-10-d_status.settings._AWARENESS_TIME)/2))/DT_DMON)].names[0] == EventName.driverUnresponsive
# engaged, down to orange, driver pays attention, back to normal; then down to orange, driver touches wheel
# - should have short orange recovery time and no green afterwards; wheel touch only recovers when paying attention
@@ -111,12 +107,12 @@ class TestMonitoring(unittest.TestCase):
interaction_vector = [car_interaction_NOT_DETECTED] * int(DISTRACTED_SECONDS_TO_ORANGE*3/DT_DMON) + \
[car_interaction_DETECTED] * (int(TEST_TIMESPAN/DT_DMON)-int(DISTRACTED_SECONDS_TO_ORANGE*3/DT_DMON))
events, _ = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
- self.assertEqual(len(events[int(DISTRACTED_SECONDS_TO_ORANGE*0.5/DT_DMON)]), 0)
- self.assertEqual(events[int((DISTRACTED_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0], EventName.promptDriverDistracted)
- self.assertEqual(len(events[int(DISTRACTED_SECONDS_TO_ORANGE*1.5/DT_DMON)]), 0)
- self.assertEqual(events[int((DISTRACTED_SECONDS_TO_ORANGE*3-0.1)/DT_DMON)].names[0], EventName.promptDriverDistracted)
- self.assertEqual(events[int((DISTRACTED_SECONDS_TO_ORANGE*3+0.1)/DT_DMON)].names[0], EventName.promptDriverDistracted)
- self.assertEqual(len(events[int((DISTRACTED_SECONDS_TO_ORANGE*3+2.5)/DT_DMON)]), 0)
+ assert len(events[int(DISTRACTED_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
+ assert events[int((DISTRACTED_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.promptDriverDistracted
+ assert len(events[int(DISTRACTED_SECONDS_TO_ORANGE*1.5/DT_DMON)]) == 0
+ assert events[int((DISTRACTED_SECONDS_TO_ORANGE*3-0.1)/DT_DMON)].names[0] == EventName.promptDriverDistracted
+ assert events[int((DISTRACTED_SECONDS_TO_ORANGE*3+0.1)/DT_DMON)].names[0] == EventName.promptDriverDistracted
+ assert len(events[int((DISTRACTED_SECONDS_TO_ORANGE*3+2.5)/DT_DMON)]) == 0
# engaged, down to orange, driver dodges camera, then comes back still distracted, down to red, \
# driver dodges, and then touches wheel to no avail, disengages and reengages
@@ -135,10 +131,10 @@ class TestMonitoring(unittest.TestCase):
op_vector[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+2.5)/DT_DMON):int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3)/DT_DMON)] \
= [False] * int(0.5/DT_DMON)
events, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
- self.assertEqual(events[int((DISTRACTED_SECONDS_TO_ORANGE+0.5*_invisible_time)/DT_DMON)].names[0], EventName.promptDriverDistracted)
- self.assertEqual(events[int((DISTRACTED_SECONDS_TO_RED+1.5*_invisible_time)/DT_DMON)].names[0], EventName.driverDistracted)
- self.assertEqual(events[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+1.5)/DT_DMON)].names[0], EventName.driverDistracted)
- self.assertTrue(len(events[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3.5)/DT_DMON)]) == 0)
+ assert events[int((DISTRACTED_SECONDS_TO_ORANGE+0.5*_invisible_time)/DT_DMON)].names[0] == EventName.promptDriverDistracted
+ assert events[int((DISTRACTED_SECONDS_TO_RED+1.5*_invisible_time)/DT_DMON)].names[0] == EventName.driverDistracted
+ assert events[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+1.5)/DT_DMON)].names[0] == EventName.driverDistracted
+ assert len(events[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3.5)/DT_DMON)]) == 0
# engaged, invisible driver, down to orange, driver touches wheel; then down to orange again, driver appears
# - both actions should clear the alert, but momentary appearance should not
@@ -150,15 +146,15 @@ class TestMonitoring(unittest.TestCase):
[msg_ATTENTIVE] * int(_visible_time/DT_DMON)
interaction_vector[int((INVISIBLE_SECONDS_TO_ORANGE)/DT_DMON):int((INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON)] = [True] * int(1/DT_DMON)
events, _ = self._run_seq(ds_vector, interaction_vector, 2*always_true, 2*always_false)
- self.assertTrue(len(events[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0)
- self.assertEqual(events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0], EventName.promptDriverUnresponsive)
- self.assertTrue(len(events[int((INVISIBLE_SECONDS_TO_ORANGE+0.1)/DT_DMON)]) == 0)
+ assert len(events[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
+ assert events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.promptDriverUnresponsive
+ assert len(events[int((INVISIBLE_SECONDS_TO_ORANGE+0.1)/DT_DMON)]) == 0
if _visible_time == 0.5:
- self.assertEqual(events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0], EventName.promptDriverUnresponsive)
- self.assertEqual(events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)].names[0], EventName.preDriverUnresponsive)
+ assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0] == EventName.promptDriverUnresponsive
+ assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)].names[0] == EventName.preDriverUnresponsive
elif _visible_time == 10:
- self.assertEqual(events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0], EventName.promptDriverUnresponsive)
- self.assertTrue(len(events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)]) == 0)
+ assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0] == EventName.promptDriverUnresponsive
+ assert len(events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)]) == 0
# engaged, invisible driver, down to red, driver appears and then touches wheel, then disengages/reengages
# - only disengage will clear the alert
@@ -171,18 +167,18 @@ class TestMonitoring(unittest.TestCase):
interaction_vector[int((INVISIBLE_SECONDS_TO_RED+_visible_time)/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time+1)/DT_DMON)] = [True] * int(1/DT_DMON)
op_vector[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1)/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)] = [False] * int(0.5/DT_DMON)
events, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
- self.assertTrue(len(events[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0)
- self.assertEqual(events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0], EventName.promptDriverUnresponsive)
- self.assertEqual(events[int((INVISIBLE_SECONDS_TO_RED-0.1)/DT_DMON)].names[0], EventName.driverUnresponsive)
- self.assertEqual(events[int((INVISIBLE_SECONDS_TO_RED+0.5*_visible_time)/DT_DMON)].names[0], EventName.driverUnresponsive)
- self.assertEqual(events[int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)].names[0], EventName.driverUnresponsive)
- self.assertTrue(len(events[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1+0.1)/DT_DMON)]) == 0)
+ assert len(events[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
+ assert events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.promptDriverUnresponsive
+ assert events[int((INVISIBLE_SECONDS_TO_RED-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive
+ assert events[int((INVISIBLE_SECONDS_TO_RED+0.5*_visible_time)/DT_DMON)].names[0] == EventName.driverUnresponsive
+ assert events[int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)].names[0] == EventName.driverUnresponsive
+ assert len(events[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1+0.1)/DT_DMON)]) == 0
# disengaged, always distracted driver
# - dm should stay quiet when not engaged
def test_pure_dashcam_user(self):
events, _ = self._run_seq(always_distracted, always_false, always_false, always_false)
- self.assertTrue(sum(len(event) for event in events) == 0)
+ assert sum(len(event) for event in events) == 0
# engaged, car stops at traffic light, down to orange, no action, then car starts moving
# - should only reach green when stopped, but continues counting down on launch
@@ -191,10 +187,10 @@ class TestMonitoring(unittest.TestCase):
standstill_vector = always_true[:]
standstill_vector[int(_redlight_time/DT_DMON):] = [False] * int((TEST_TIMESPAN-_redlight_time)/DT_DMON)
events, d_status = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
- self.assertEqual(events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL+1)/DT_DMON)].names[0],
- EventName.preDriverDistracted)
- self.assertEqual(events[int((_redlight_time-0.1)/DT_DMON)].names[0], EventName.preDriverDistracted)
- self.assertEqual(events[int((_redlight_time+0.5)/DT_DMON)].names[0], EventName.promptDriverDistracted)
+ assert events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL+1)/DT_DMON)].names[0] == \
+ EventName.preDriverDistracted
+ assert events[int((_redlight_time-0.1)/DT_DMON)].names[0] == EventName.preDriverDistracted
+ assert events[int((_redlight_time+0.5)/DT_DMON)].names[0] == EventName.promptDriverDistracted
# engaged, model is somehow uncertain and driver is distracted
# - should fall back to wheel touch after uncertain alert
@@ -202,13 +198,10 @@ class TestMonitoring(unittest.TestCase):
ds_vector = [msg_DISTRACTED_BUT_SOMEHOW_UNCERTAIN] * int(TEST_TIMESPAN/DT_DMON)
interaction_vector = always_false[:]
events, d_status = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
- self.assertTrue(EventName.preDriverUnresponsive in
- events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME-0.1)/DT_DMON)].names)
- self.assertTrue(EventName.promptDriverUnresponsive in
- events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names)
- self.assertTrue(EventName.driverUnresponsive in
- events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names)
+ assert EventName.preDriverUnresponsive in \
+ events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME-0.1)/DT_DMON)].names
+ assert EventName.promptDriverUnresponsive in \
+ events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
+ assert EventName.driverUnresponsive in \
+ events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/navd/tests/test_map_renderer.py b/selfdrive/navd/tests/test_map_renderer.py
index 832e0d1eab..52b594a57e 100755
--- a/selfdrive/navd/tests/test_map_renderer.py
+++ b/selfdrive/navd/tests/test_map_renderer.py
@@ -3,7 +3,6 @@ import time
import numpy as np
import os
import pytest
-import unittest
import requests
import threading
import http.server
@@ -66,11 +65,11 @@ class MapBoxInternetDisabledServer(threading.Thread):
@pytest.mark.skip(reason="not used")
-class TestMapRenderer(unittest.TestCase):
+class TestMapRenderer:
server: MapBoxInternetDisabledServer
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
assert "MAPBOX_TOKEN" in os.environ
cls.original_token = os.environ["MAPBOX_TOKEN"]
cls.server = MapBoxInternetDisabledServer()
@@ -78,10 +77,10 @@ class TestMapRenderer(unittest.TestCase):
time.sleep(0.5) # wait for server to startup
@classmethod
- def tearDownClass(cls) -> None:
+ def teardown_class(cls) -> None:
cls.server.stop()
- def setUp(self):
+ def setup_method(self):
self.server.enable_internet()
os.environ['MAPS_HOST'] = f'http://localhost:{self.server.port}'
@@ -203,15 +202,12 @@ class TestMapRenderer(unittest.TestCase):
def assert_stat(stat, nominal, tol=0.3):
tol = (nominal / (1+tol)), (nominal * (1+tol))
- self.assertTrue(tol[0] < stat < tol[1], f"{stat} not in tolerance {tol}")
+ assert tol[0] < stat < tol[1], f"{stat} not in tolerance {tol}"
assert_stat(_mean, 0.030)
assert_stat(_median, 0.027)
assert_stat(_stddev, 0.0078)
- self.assertLess(_max, 0.065)
- self.assertGreater(_min, 0.015)
+ assert _max < 0.065
+ assert _min > 0.015
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/navd/tests/test_navd.py b/selfdrive/navd/tests/test_navd.py
index 61be6cc387..3d899ba282 100755
--- a/selfdrive/navd/tests/test_navd.py
+++ b/selfdrive/navd/tests/test_navd.py
@@ -1,22 +1,21 @@
#!/usr/bin/env python3
import json
import random
-import unittest
import numpy as np
from parameterized import parameterized
import cereal.messaging as messaging
from openpilot.common.params import Params
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
-class TestNavd(unittest.TestCase):
- def setUp(self):
+class TestNavd:
+ def setup_method(self):
self.params = Params()
self.sm = messaging.SubMaster(['navRoute', 'navInstruction'])
- def tearDown(self):
+ def teardown_method(self):
managed_processes['navd'].stop()
def _check_route(self, start, end, check_coords=True):
@@ -57,7 +56,3 @@ class TestNavd(unittest.TestCase):
start = {"latitude": random.uniform(-90, 90), "longitude": random.uniform(-180, 180)}
end = {"latitude": random.uniform(-90, 90), "longitude": random.uniform(-180, 180)}
self._check_route(start, end, check_coords=False)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/test/docker_build.sh b/selfdrive/test/docker_build.sh
index 5f77ceb10d..4d58a1507c 100755
--- a/selfdrive/test/docker_build.sh
+++ b/selfdrive/test/docker_build.sh
@@ -17,7 +17,7 @@ fi
source $SCRIPT_DIR/docker_common.sh $1 "$TAG_SUFFIX"
-DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load --cache-to type=inline --cache-from type=registry,ref=$REMOTE_TAG -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR
+DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load --cache-to type=inline --cache-from type=registry,ref=$REMOTE_TAG -t $DOCKER_IMAGE:latest -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR
if [ -n "$PUSH_IMAGE" ]; then
docker push $REMOTE_TAG
diff --git a/selfdrive/test/helpers.py b/selfdrive/test/helpers.py
index ce8918aec3..b7f5bb183b 100644
--- a/selfdrive/test/helpers.py
+++ b/selfdrive/test/helpers.py
@@ -3,12 +3,13 @@ import http.server
import os
import threading
import time
+import pytest
from functools import wraps
import cereal.messaging as messaging
from openpilot.common.params import Params
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
from openpilot.system.hardware import PC
from openpilot.system.version import training_version, terms_version
@@ -32,15 +33,15 @@ def phone_only(f):
@wraps(f)
def wrap(self, *args, **kwargs):
if PC:
- self.skipTest("This test is not meant to run on PC")
- f(self, *args, **kwargs)
+ pytest.skip("This test is not meant to run on PC")
+ return f(self, *args, **kwargs)
return wrap
def release_only(f):
@wraps(f)
def wrap(self, *args, **kwargs):
if "RELEASE" not in os.environ:
- self.skipTest("This test is only for release branches")
+ pytest.skip("This test is only for release branches")
f(self, *args, **kwargs)
return wrap
diff --git a/selfdrive/test/longitudinal_maneuvers/plant.py b/selfdrive/test/longitudinal_maneuvers/plant.py
index ac54967f88..daf7cec32b 100755
--- a/selfdrive/test/longitudinal_maneuvers/plant.py
+++ b/selfdrive/test/longitudinal_maneuvers/plant.py
@@ -83,7 +83,7 @@ class Plant:
lead = log.RadarState.LeadData.new_message()
lead.dRel = float(d_rel)
- lead.yRel = float(0.0)
+ lead.yRel = 0.0
lead.vRel = float(v_rel)
lead.aRel = float(a_lead - self.acceleration)
lead.vLead = float(v_lead)
diff --git a/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py b/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py
index 713b7801f8..0ad6d6d4fd 100755
--- a/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py
+++ b/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import itertools
-import unittest
from parameterized import parameterized_class
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import STOP_DISTANCE
@@ -144,17 +143,13 @@ def create_maneuvers(kwargs):
@parameterized_class(("e2e", "force_decel"), itertools.product([True, False], repeat=2))
-class LongitudinalControl(unittest.TestCase):
+class TestLongitudinalControl:
e2e: bool
force_decel: bool
- def test_maneuver(self):
+ def test_maneuver(self, subtests):
for maneuver in create_maneuvers({"e2e": self.e2e, "force_decel": self.force_decel}):
- with self.subTest(title=maneuver.title, e2e=maneuver.e2e, force_decel=maneuver.force_decel):
+ with subtests.test(title=maneuver.title, e2e=maneuver.e2e, force_decel=maneuver.force_decel):
print(maneuver.title, f'in {"e2e" if maneuver.e2e else "acc"} mode')
valid, _ = maneuver.evaluate()
- self.assertTrue(valid)
-
-
-if __name__ == "__main__":
- unittest.main(failfast=True)
+ assert valid
diff --git a/selfdrive/test/process_replay/migration.py b/selfdrive/test/process_replay/migration.py
index 377897448d..cba6581599 100644
--- a/selfdrive/test/process_replay/migration.py
+++ b/selfdrive/test/process_replay/migration.py
@@ -4,7 +4,7 @@ from cereal import messaging
from openpilot.selfdrive.car.fingerprints import MIGRATION
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_encode_index
from openpilot.selfdrive.car.toyota.values import EPS_SCALE
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
from panda import Panda
@@ -14,6 +14,7 @@ def migrate_all(lr, old_logtime=False, manager_states=False, panda_states=False,
msgs = migrate_carParams(msgs, old_logtime)
msgs = migrate_gpsLocation(msgs)
msgs = migrate_deviceState(msgs)
+ msgs = migrate_carOutput(msgs)
if manager_states:
msgs = migrate_managerState(msgs)
if panda_states:
@@ -69,6 +70,23 @@ def migrate_deviceState(lr):
return all_msgs
+def migrate_carOutput(lr):
+ # migration needed only for routes before carOutput
+ if any(msg.which() == 'carOutput' for msg in lr):
+ return lr
+
+ all_msgs = []
+ for msg in lr:
+ if msg.which() == 'carControl':
+ co = messaging.new_message('carOutput')
+ co.valid = msg.valid
+ co.logMonoTime = msg.logMonoTime
+ co.carOutput.actuatorsOutput = msg.carControl.actuatorsOutputDEPRECATED
+ all_msgs.append(co.as_reader())
+ all_msgs.append(msg)
+ return all_msgs
+
+
def migrate_pandaStates(lr):
all_msgs = []
# TODO: safety param migration should be handled automatically
@@ -183,9 +201,7 @@ def migrate_carParams(lr, old_logtime=False):
all_msgs = []
for msg in lr:
if msg.which() == 'carParams':
- CP = messaging.new_message('carParams')
- CP.valid = True
- CP.carParams = msg.carParams.as_builder()
+ CP = msg.as_builder()
CP.carParams.carFingerprint = MIGRATION.get(CP.carParams.carFingerprint, CP.carParams.carFingerprint)
for car_fw in CP.carParams.carFw:
car_fw.brand = CP.carParams.carName
diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit
index 47571656e5..87f1adfada 100644
--- a/selfdrive/test/process_replay/model_replay_ref_commit
+++ b/selfdrive/test/process_replay/model_replay_ref_commit
@@ -1 +1 @@
-69a3b169ebc478285dad5eea87658ed2cb8fee13
+73eb11111fb6407fa307c3f2bdd3331f2514c707
diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py
index 233f5d746a..b325e6e693 100755
--- a/selfdrive/test/process_replay/process_replay.py
+++ b/selfdrive/test/process_replay/process_replay.py
@@ -6,7 +6,7 @@ import json
import heapq
import signal
import platform
-from collections import OrderedDict
+from collections import Counter, OrderedDict
from dataclasses import dataclass, field
from typing import Any
from collections.abc import Callable, Iterable
@@ -23,7 +23,7 @@ from openpilot.common.timeout import Timeout
from openpilot.common.realtime import DT_CTRL
from panda.python import ALTERNATIVE_EXPERIENCE
from openpilot.selfdrive.car.car_helpers import get_car, interfaces
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state, available_streams
from openpilot.selfdrive.test.process_replay.migration import migrate_all
from openpilot.selfdrive.test.process_replay.capture import ProcessOutputCapture
@@ -317,12 +317,12 @@ class ProcessContainer:
return output_msgs
-def controlsd_fingerprint_callback(rc, pm, msgs, fingerprint):
+def card_fingerprint_callback(rc, pm, msgs, fingerprint):
print("start fingerprinting")
params = Params()
canmsgs = [msg for msg in msgs if msg.which() == "can"][:300]
- # controlsd expects one arbitrary can and pandaState
+ # card expects one arbitrary can and pandaState
rc.send_sync(pm, "can", messaging.new_message("can", 1))
pm.send("pandaStates", messaging.new_message("pandaStates", 1))
rc.send_sync(pm, "can", messaging.new_message("can", 1))
@@ -356,12 +356,20 @@ def get_car_params_callback(rc, pm, msgs, fingerprint):
for m in canmsgs[:300]:
can.send(m.as_builder().to_bytes())
_, CP = get_car(can, sendcan, Params().get_bool("ExperimentalLongitudinalEnabled"))
+
+ if not params.get_bool("DisengageOnAccelerator"):
+ CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS
+
params.put("CarParams", CP.to_bytes())
return CP
def controlsd_rcv_callback(msg, cfg, frame):
- # no sendcan until controlsd is initialized
+ return (frame - 1) == 0 or msg.which() == 'carState'
+
+
+def card_rcv_callback(msg, cfg, frame):
+ # no sendcan until card is initialized
if msg.which() != "can":
return False
@@ -461,18 +469,28 @@ CONFIGS = [
ProcessConfig(
proc_name="controlsd",
pubs=[
- "can", "deviceState", "pandaStates", "peripheralState", "liveCalibration", "driverMonitoringState",
+ "carState", "deviceState", "pandaStates", "peripheralState", "liveCalibration", "driverMonitoringState",
"longitudinalPlan", "liveLocationKalman", "liveParameters", "radarState",
"modelV2", "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState",
- "testJoystick", "liveTorqueParameters", "accelerometer", "gyroscope"
+ "testJoystick", "liveTorqueParameters", "accelerometer", "gyroscope", "carOutput"
],
- subs=["controlsState", "carState", "carControl", "sendcan", "onroadEvents", "carParams"],
+ subs=["controlsState", "carControl", "onroadEvents"],
ignore=["logMonoTime", "controlsState.startMonoTime", "controlsState.cumLagMs"],
config_callback=controlsd_config_callback,
- init_callback=controlsd_fingerprint_callback,
+ init_callback=get_car_params_callback,
should_recv_callback=controlsd_rcv_callback,
tolerance=NUMPY_TOLERANCE,
processing_time=0.004,
+ ),
+ ProcessConfig(
+ proc_name="card",
+ pubs=["pandaStates", "carControl", "onroadEvents", "can"],
+ subs=["sendcan", "carState", "carParams", "carOutput"],
+ ignore=["logMonoTime", "carState.cumLagMs"],
+ init_callback=card_fingerprint_callback,
+ should_recv_callback=card_rcv_callback,
+ tolerance=NUMPY_TOLERANCE,
+ processing_time=0.004,
main_pub="can",
),
ProcessConfig(
@@ -537,7 +555,7 @@ CONFIGS = [
),
ProcessConfig(
proc_name="torqued",
- pubs=["liveLocationKalman", "carState", "carControl"],
+ pubs=["liveLocationKalman", "carState", "carControl", "carOutput"],
subs=["liveTorqueParameters"],
ignore=["logMonoTime"],
init_callback=get_car_params_callback,
@@ -798,3 +816,18 @@ def check_openpilot_enabled(msgs: LogIterable) -> bool:
max_enabled_count = max(max_enabled_count, cur_enabled_count)
return max_enabled_count > int(10. / DT_CTRL)
+
+
+def check_most_messages_valid(msgs: LogIterable, threshold: float = 0.9) -> bool:
+ msgs_counts = Counter(msg.which() for msg in msgs)
+ msgs_valid_counts = Counter(msg.which() for msg in msgs if msg.valid)
+
+ most_valid_for_service = {}
+ for msg_type in msgs_counts.keys():
+ valid_share = msgs_valid_counts.get(msg_type, 0) / msgs_counts[msg_type]
+ ok = valid_share >= threshold
+ if not ok:
+ print(f"WARNING: Service {msg_type} has {valid_share * 100:.2f}% valid messages, which is below threshold of {threshold * 100:.2f}%")
+ most_valid_for_service[msg_type] = ok
+
+ return all(most_valid_for_service.values())
diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit
index a0d3cc6bb2..0a9a54325a 100644
--- a/selfdrive/test/process_replay/ref_commit
+++ b/selfdrive/test/process_replay/ref_commit
@@ -1 +1 @@
-685a2bb9aacdd790e26d14aa49d3792c3ed65125
\ No newline at end of file
+87aa5052e36d5cf83698b1eb6e50aef5c86df8ca
\ No newline at end of file
diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py
index ec3023c5dc..bf7a4bfd97 100755
--- a/selfdrive/test/process_replay/regen.py
+++ b/selfdrive/test/process_replay/regen.py
@@ -9,7 +9,7 @@ from typing import Any
from collections.abc import Iterable
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, ProcessConfig, replay_process, get_process_config, \
- check_openpilot_enabled, get_custom_params_from_lr
+ check_openpilot_enabled, check_most_messages_valid, get_custom_params_from_lr
from openpilot.selfdrive.test.process_replay.vision_meta import DRIVER_CAMERA_FRAME_SIZES
from openpilot.selfdrive.test.update_ci_routes import upload_route
from openpilot.tools.lib.route import Route
@@ -129,6 +129,8 @@ def regen_and_save(
if not check_openpilot_enabled(output_logs):
raise Exception("Route did not engage for long enough")
+ if not check_most_messages_valid(output_logs):
+ raise Exception("Route has too many invalid messages")
if upload:
upload_route(rel_log_dir)
diff --git a/selfdrive/test/process_replay/test_fuzzy.py b/selfdrive/test/process_replay/test_fuzzy.py
index 6c81119fbf..6bcc94911f 100755
--- a/selfdrive/test/process_replay/test_fuzzy.py
+++ b/selfdrive/test/process_replay/test_fuzzy.py
@@ -3,7 +3,6 @@ import copy
from hypothesis import given, HealthCheck, Phase, settings
import hypothesis.strategies as st
from parameterized import parameterized
-import unittest
from cereal import log
from openpilot.selfdrive.car.toyota.values import CAR as TOYOTA
@@ -13,11 +12,11 @@ import openpilot.selfdrive.test.process_replay.process_replay as pr
# These processes currently fail because of unrealistic data breaking assumptions
# that openpilot makes causing error with NaN, inf, int size, array indexing ...
# TODO: Make each one testable
-NOT_TESTED = ['controlsd', 'plannerd', 'calibrationd', 'dmonitoringd', 'paramsd', 'dmonitoringmodeld', 'modeld']
+NOT_TESTED = ['controlsd', 'card', 'plannerd', 'calibrationd', 'dmonitoringd', 'paramsd', 'dmonitoringmodeld', 'modeld']
TEST_CASES = [(cfg.proc_name, copy.deepcopy(cfg)) for cfg in pr.CONFIGS if cfg.proc_name not in NOT_TESTED]
-class TestFuzzProcesses(unittest.TestCase):
+class TestFuzzProcesses:
# TODO: make this faster and increase examples
@parameterized.expand(TEST_CASES)
@@ -28,6 +27,3 @@ class TestFuzzProcesses(unittest.TestCase):
lr = [log.Event.new_message(**m).as_reader() for m in msgs]
cfg.timeout = 5
pr.replay_process(cfg, lr, fingerprint=TOYOTA.TOYOTA_COROLLA_TSS2, disable_progress=True)
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/test/process_replay/test_imgproc.py b/selfdrive/test/process_replay/test_imgproc.py
index a980548baa..036f21b9e5 100755
--- a/selfdrive/test/process_replay/test_imgproc.py
+++ b/selfdrive/test/process_replay/test_imgproc.py
@@ -93,7 +93,7 @@ if __name__ == "__main__":
if pix_hash != ref_hash:
print("result changed! please check kernel")
- print("ref: %s" % ref_hash)
- print("new: %s" % pix_hash)
+ print(f"ref: {ref_hash}")
+ print(f"new: {pix_hash}")
else:
print("test passed")
diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py
index 5fa80f0e09..533ab125f9 100755
--- a/selfdrive/test/process_replay/test_processes.py
+++ b/selfdrive/test/process_replay/test_processes.py
@@ -11,7 +11,8 @@ from openpilot.common.git import get_commit
from openpilot.selfdrive.car.car_helpers import interface_names
from openpilot.tools.lib.openpilotci import get_url, upload_file
from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff
-from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, PROC_REPLAY_DIR, FAKEDATA, check_openpilot_enabled, replay_process
+from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, PROC_REPLAY_DIR, FAKEDATA, replay_process, \
+ check_openpilot_enabled, check_most_messages_valid
from openpilot.tools.lib.filereader import FileReader
from openpilot.tools.lib.logreader import LogReader
from openpilot.tools.lib.helpers import save_log
@@ -41,23 +42,23 @@ source_segments = [
]
segments = [
- ("BODY", "regen997DF2697CB|2023-10-30--23-14-29--0"),
- ("HYUNDAI", "regen2A9D2A8E0B4|2023-10-30--23-13-34--0"),
- ("HYUNDAI2", "regen6CA24BC3035|2023-10-30--23-14-28--0"),
- ("TOYOTA", "regen5C019D76307|2023-10-30--23-13-31--0"),
- ("TOYOTA2", "regen5DCADA88A96|2023-10-30--23-14-57--0"),
- ("TOYOTA3", "regen7204CA3A498|2023-10-30--23-15-55--0"),
- ("HONDA", "regen048F8FA0B24|2023-10-30--23-15-53--0"),
- ("HONDA2", "regen7D2D3F82D5B|2023-10-30--23-15-55--0"),
- ("CHRYSLER", "regen7125C42780C|2023-10-30--23-16-21--0"),
- ("RAM", "regen2731F3213D2|2023-10-30--23-18-11--0"),
- ("SUBARU", "regen86E4C1B4DDD|2023-10-30--23-18-14--0"),
- ("GM", "regenF6393D64745|2023-10-30--23-17-18--0"),
- ("GM2", "regen220F830C05B|2023-10-30--23-18-39--0"),
- ("NISSAN", "regen4F671F7C435|2023-10-30--23-18-40--0"),
- ("VOLKSWAGEN", "regen8BDFE7307A0|2023-10-30--23-19-36--0"),
- ("MAZDA", "regen2E9F1A15FD5|2023-10-30--23-20-36--0"),
- ("FORD", "regen6D39E54606E|2023-10-30--23-20-54--0"),
+ ("BODY", "regen29FD9FF7760|2024-05-21--06-58-51--0"),
+ ("HYUNDAI", "regen0B1B76A1C27|2024-05-21--06-57-53--0"),
+ ("HYUNDAI2", "regen3BB55FA5E20|2024-05-21--06-59-03--0"),
+ ("TOYOTA", "regenF6FB954C1E2|2024-05-21--06-57-53--0"),
+ ("TOYOTA2", "regen0AC637CE7BA|2024-05-21--06-57-54--0"),
+ ("TOYOTA3", "regenC7BE3FAE496|2024-05-21--06-59-01--0"),
+ ("HONDA", "regen58E9F8B695A|2024-05-21--06-57-55--0"),
+ ("HONDA2", "regen8695608EB15|2024-05-21--06-57-55--0"),
+ ("CHRYSLER", "regenB0F8C25C902|2024-05-21--06-59-47--0"),
+ ("RAM", "regenB3B2C7A105B|2024-05-21--07-00-47--0"),
+ ("SUBARU", "regen860FD736DCC|2024-05-21--07-00-50--0"),
+ ("GM", "regen8CB3048DEB9|2024-05-21--06-59-49--0"),
+ ("GM2", "regen379D446541D|2024-05-21--07-00-51--0"),
+ ("NISSAN", "regen24871108F80|2024-05-21--07-00-38--0"),
+ ("VOLKSWAGEN", "regenF390392F275|2024-05-21--07-00-52--0"),
+ ("MAZDA", "regenE5A36020581|2024-05-21--07-01-51--0"),
+ ("FORD", "regenDC288ED0D78|2024-05-21--07-02-18--0"),
]
# dashcamOnly makes don't need to be tested until a full port is done
@@ -107,9 +108,15 @@ def test_process(cfg, lr, segment, ref_log_path, new_log_path, ignore_fields=Non
# check to make sure openpilot is engaged in the route
if cfg.proc_name == "controlsd":
if not check_openpilot_enabled(log_msgs):
- # FIXME: these segments should work, but the replay enabling logic is too brittle
- if segment not in ("regen6CA24BC3035|2023-10-30--23-14-28--0", "regen7D2D3F82D5B|2023-10-30--23-15-55--0"):
- return f"Route did not enable at all or for long enough: {new_log_path}", log_msgs
+ return f"Route did not enable at all or for long enough: {new_log_path}", log_msgs
+ if not check_most_messages_valid(log_msgs):
+ return f"Route did not have enough valid messages: {new_log_path}", log_msgs
+
+ if cfg.proc_name != 'ubloxd' or segment != 'regen3BB55FA5E20|2024-05-21--06-59-03--0':
+ seen_msgs = {m.which() for m in log_msgs}
+ expected_msgs = set(cfg.subs)
+ if seen_msgs != expected_msgs:
+ return f"Expected messages: {expected_msgs}, but got: {seen_msgs}", log_msgs
try:
return compare_logs(ref_log_msgs, log_msgs, ignore_fields + cfg.ignore, ignore_msgs, cfg.tolerance), log_msgs
diff --git a/selfdrive/test/process_replay/test_regen.py b/selfdrive/test/process_replay/test_regen.py
index d989635497..c27d9e8f7b 100755
--- a/selfdrive/test/process_replay/test_regen.py
+++ b/selfdrive/test/process_replay/test_regen.py
@@ -1,7 +1,5 @@
#!/usr/bin/env python3
-import unittest
-
from parameterized import parameterized
from openpilot.selfdrive.test.process_replay.regen import regen_segment, DummyFrameReader
@@ -30,7 +28,7 @@ def ci_setup_data_readers(route, sidx):
return lr, frs
-class TestRegen(unittest.TestCase):
+class TestRegen:
@parameterized.expand(TESTED_SEGMENTS)
def test_engaged(self, case_name, segment):
route, sidx = segment.rsplit("--", 1)
@@ -38,8 +36,4 @@ class TestRegen(unittest.TestCase):
output_logs = regen_segment(lr, frs, disable_tqdm=True)
engaged = check_openpilot_enabled(output_logs)
- self.assertTrue(engaged, f"openpilot not engaged in {case_name}")
-
-
-if __name__=='__main__':
- unittest.main()
+ assert engaged, f"openpilot not engaged in {case_name}"
diff --git a/selfdrive/test/profiling/lib.py b/selfdrive/test/profiling/lib.py
index 7f3b0126ff..93ba215386 100644
--- a/selfdrive/test/profiling/lib.py
+++ b/selfdrive/test/profiling/lib.py
@@ -8,7 +8,7 @@ class ReplayDone(Exception):
pass
-class SubSocket():
+class SubSocket:
def __init__(self, msgs, trigger):
self.i = 0
self.trigger = trigger
@@ -28,7 +28,7 @@ class SubSocket():
return msg
-class PubSocket():
+class PubSocket:
def send(self, data):
pass
diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py
index cd0846c894..8b60b9650b 100755
--- a/selfdrive/test/test_onroad.py
+++ b/selfdrive/test/test_onroad.py
@@ -10,7 +10,6 @@ import shutil
import subprocess
import time
import numpy as np
-import unittest
from collections import Counter, defaultdict
from functools import cached_property
from pathlib import Path
@@ -36,7 +35,8 @@ CPU usage budget
MAX_TOTAL_CPU = 250. # total for all 8 cores
PROCS = {
# Baseline CPU usage by process
- "selfdrive.controls.controlsd": 46.0,
+ "selfdrive.controls.controlsd": 32.0,
+ "selfdrive.car.card": 22.0,
"./loggerd": 14.0,
"./encoderd": 17.0,
"./camerad": 14.5,
@@ -48,19 +48,19 @@ PROCS = {
"selfdrive.controls.radard": 7.0,
"selfdrive.modeld.modeld": 13.0,
"selfdrive.modeld.dmonitoringmodeld": 8.0,
- "selfdrive.thermald.thermald": 3.87,
+ "system.thermald.thermald": 3.87,
"selfdrive.locationd.calibrationd": 2.0,
"selfdrive.locationd.torqued": 5.0,
"selfdrive.ui.soundd": 3.5,
"selfdrive.monitoring.dmonitoringd": 4.0,
"./proclogd": 1.54,
"system.logmessaged": 0.2,
- "selfdrive.tombstoned": 0,
+ "system.tombstoned": 0,
"./logcatd": 0,
"system.micd": 6.0,
"system.timed": 0,
"selfdrive.boardd.pandad": 0,
- "selfdrive.statsd": 0.4,
+ "system.statsd": 0.4,
"selfdrive.navd.navd": 0.4,
"system.loggerd.uploader": (0.5, 15.0),
"system.loggerd.deleter": 0.1,
@@ -102,10 +102,10 @@ def cputime_total(ct):
@pytest.mark.tici
-class TestOnroad(unittest.TestCase):
+class TestOnroad:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
if "DEBUG" in os.environ:
segs = filter(lambda x: os.path.exists(os.path.join(x, "rlog")), Path(Paths.log_root()).iterdir())
segs = sorted(segs, key=lambda x: x.stat().st_mtime)
@@ -125,7 +125,7 @@ class TestOnroad(unittest.TestCase):
# start manager and run openpilot for a minute
proc = None
try:
- manager_path = os.path.join(BASEDIR, "selfdrive/manager/manager.py")
+ manager_path = os.path.join(BASEDIR, "system/manager/manager.py")
proc = subprocess.Popen(["python", manager_path])
sm = messaging.SubMaster(['carState'])
@@ -181,7 +181,7 @@ class TestOnroad(unittest.TestCase):
msgs[m.which()].append(m)
return msgs
- def test_service_frequencies(self):
+ def test_service_frequencies(self, subtests):
for s, msgs in self.service_msgs.items():
if s in ('initData', 'sentinel'):
continue
@@ -190,18 +190,18 @@ class TestOnroad(unittest.TestCase):
if s in ('ubloxGnss', 'ubloxRaw', 'gnssMeasurements', 'gpsLocation', 'gpsLocationExternal', 'qcomGnss'):
continue
- with self.subTest(service=s):
+ with subtests.test(service=s):
assert len(msgs) >= math.floor(SERVICE_LIST[s].frequency*55)
def test_cloudlog_size(self):
msgs = [m for m in self.lr if m.which() == 'logMessage']
total_size = sum(len(m.as_builder().to_bytes()) for m in msgs)
- self.assertLess(total_size, 3.5e5)
+ assert total_size < 3.5e5
cnt = Counter(json.loads(m.logMessage)['filename'] for m in msgs)
big_logs = [f for f, n in cnt.most_common(3) if n / sum(cnt.values()) > 30.]
- self.assertEqual(len(big_logs), 0, f"Log spam: {big_logs}")
+ assert len(big_logs) == 0, f"Log spam: {big_logs}"
def test_log_sizes(self):
for f, sz in self.log_sizes.items():
@@ -230,15 +230,15 @@ class TestOnroad(unittest.TestCase):
result += "------------------------------------------------\n"
print(result)
- self.assertLess(max(ts), 250.)
- self.assertLess(np.mean(ts), 10.)
+ assert max(ts) < 250.
+ assert np.mean(ts) < 10.
#self.assertLess(np.std(ts), 5.)
# some slow frames are expected since camerad/modeld can preempt ui
veryslow = [x for x in ts if x > 40.]
assert len(veryslow) < 5, f"Too many slow frame draw times: {veryslow}"
- def test_cpu_usage(self):
+ def test_cpu_usage(self, subtests):
result = "\n"
result += "------------------------------------------------\n"
result += "------------------ CPU Usage -------------------\n"
@@ -286,12 +286,12 @@ class TestOnroad(unittest.TestCase):
# Ensure there's no missing procs
all_procs = {p.name for p in self.service_msgs['managerState'][0].managerState.processes if p.shouldBeRunning}
for p in all_procs:
- with self.subTest(proc=p):
+ with subtests.test(proc=p):
assert any(p in pp for pp in PROCS.keys()), f"Expected CPU usage missing for {p}"
# total CPU check
procs_tot = sum([(max(x) if isinstance(x, tuple) else x) for x in PROCS.values()])
- with self.subTest(name="total CPU"):
+ with subtests.test(name="total CPU"):
assert procs_tot < MAX_TOTAL_CPU, "Total CPU budget exceeded"
result += "------------------------------------------------\n"
result += f"Total allocated CPU usage is {procs_tot}%, budget is {MAX_TOTAL_CPU}%, {MAX_TOTAL_CPU-procs_tot:.1f}% left\n"
@@ -299,7 +299,7 @@ class TestOnroad(unittest.TestCase):
print(result)
- self.assertTrue(cpu_ok)
+ assert cpu_ok
def test_memory_usage(self):
mems = [m.deviceState.memoryUsagePercent for m in self.service_msgs['deviceState']]
@@ -307,10 +307,10 @@ class TestOnroad(unittest.TestCase):
# check for big leaks. note that memory usage is
# expected to go up while the MSGQ buffers fill up
- self.assertLessEqual(max(mems) - min(mems), 3.0)
+ assert max(mems) - min(mems) <= 3.0
def test_gpu_usage(self):
- self.assertEqual(self.gpu_procs, {"weston", "ui", "camerad", "selfdrive.modeld.modeld"})
+ assert self.gpu_procs == {"weston", "ui", "camerad", "selfdrive.modeld.modeld"}
def test_camera_processing_time(self):
result = "\n"
@@ -319,14 +319,14 @@ class TestOnroad(unittest.TestCase):
result += "------------------------------------------------\n"
ts = [getattr(m, m.which()).processingTime for m in self.lr if 'CameraState' in m.which()]
- self.assertLess(min(ts), 0.025, f"high execution time: {min(ts)}")
+ assert min(ts) < 0.025, f"high execution time: {min(ts)}"
result += f"execution time: min {min(ts):.5f}s\n"
result += f"execution time: max {max(ts):.5f}s\n"
result += f"execution time: mean {np.mean(ts):.5f}s\n"
result += "------------------------------------------------\n"
print(result)
- @unittest.skip("TODO: enable once timings are fixed")
+ @pytest.mark.skip("TODO: enable once timings are fixed")
def test_camera_frame_timings(self):
result = "\n"
result += "------------------------------------------------\n"
@@ -336,7 +336,7 @@ class TestOnroad(unittest.TestCase):
ts = [getattr(m, m.which()).timestampSof for m in self.lr if name in m.which()]
d_ms = np.diff(ts) / 1e6
d50 = np.abs(d_ms-50)
- self.assertLess(max(d50), 1.0, f"high sof delta vs 50ms: {max(d50)}")
+ assert max(d50) < 1.0, f"high sof delta vs 50ms: {max(d50)}"
result += f"{name} sof delta vs 50ms: min {min(d50):.5f}s\n"
result += f"{name} sof delta vs 50ms: max {max(d50):.5f}s\n"
result += f"{name} sof delta vs 50ms: mean {d50.mean():.5f}s\n"
@@ -352,8 +352,8 @@ class TestOnroad(unittest.TestCase):
cfgs = [("longitudinalPlan", 0.05, 0.05),]
for (s, instant_max, avg_max) in cfgs:
ts = [getattr(m, s).solverExecutionTime for m in self.service_msgs[s]]
- self.assertLess(max(ts), instant_max, f"high '{s}' execution time: {max(ts)}")
- self.assertLess(np.mean(ts), avg_max, f"high avg '{s}' execution time: {np.mean(ts)}")
+ assert max(ts) < instant_max, f"high '{s}' execution time: {max(ts)}"
+ assert np.mean(ts) < avg_max, f"high avg '{s}' execution time: {np.mean(ts)}"
result += f"'{s}' execution time: min {min(ts):.5f}s\n"
result += f"'{s}' execution time: max {max(ts):.5f}s\n"
result += f"'{s}' execution time: mean {np.mean(ts):.5f}s\n"
@@ -372,8 +372,8 @@ class TestOnroad(unittest.TestCase):
]
for (s, instant_max, avg_max) in cfgs:
ts = [getattr(m, s).modelExecutionTime for m in self.service_msgs[s]]
- self.assertLess(max(ts), instant_max, f"high '{s}' execution time: {max(ts)}")
- self.assertLess(np.mean(ts), avg_max, f"high avg '{s}' execution time: {np.mean(ts)}")
+ assert max(ts) < instant_max, f"high '{s}' execution time: {max(ts)}"
+ assert np.mean(ts) < avg_max, f"high avg '{s}' execution time: {np.mean(ts)}"
result += f"'{s}' execution time: min {min(ts):.5f}s\n"
result += f"'{s}' execution time: max {max(ts):.5f}s\n"
result += f"'{s}' execution time: mean {np.mean(ts):.5f}s\n"
@@ -409,7 +409,7 @@ class TestOnroad(unittest.TestCase):
result += f"{''.ljust(40)} {np.max(np.absolute([np.max(ts)/dt, np.min(ts)/dt]))} {np.std(ts)/dt}\n"
result += "="*67
print(result)
- self.assertTrue(passed)
+ assert passed
@release_only
def test_startup(self):
@@ -420,7 +420,7 @@ class TestOnroad(unittest.TestCase):
startup_alert = msg.controlsState.alertText1
break
expected = EVENTS[car.CarEvent.EventName.startup][ET.PERMANENT].alert_text_1
- self.assertEqual(startup_alert, expected, "wrong startup alert")
+ assert startup_alert == expected, "wrong startup alert"
def test_engagable(self):
no_entries = Counter()
@@ -432,7 +432,3 @@ class TestOnroad(unittest.TestCase):
eng = [m.controlsState.engageable for m in self.service_msgs['controlsState']]
assert all(eng), \
f"Not engageable for whole segment:\n- controlsState.engageable: {Counter(eng)}\n- No entry events: {no_entries}"
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/test/test_time_to_onroad.py b/selfdrive/test/test_time_to_onroad.py
index a3f803e221..11de5283b5 100755
--- a/selfdrive/test/test_time_to_onroad.py
+++ b/selfdrive/test/test_time_to_onroad.py
@@ -4,21 +4,24 @@ import pytest
import time
import subprocess
+from cereal import car
import cereal.messaging as messaging
from openpilot.common.basedir import BASEDIR
from openpilot.common.timeout import Timeout
from openpilot.selfdrive.test.helpers import set_params_enabled
+EventName = car.CarEvent.EventName
+
@pytest.mark.tici
def test_time_to_onroad():
# launch
set_params_enabled()
- manager_path = os.path.join(BASEDIR, "selfdrive/manager/manager.py")
+ manager_path = os.path.join(BASEDIR, "system/manager/manager.py")
proc = subprocess.Popen(["python", manager_path])
start_time = time.monotonic()
- sm = messaging.SubMaster(['controlsState', 'deviceState', 'onroadEvents', 'sendcan'])
+ sm = messaging.SubMaster(['controlsState', 'deviceState', 'onroadEvents'])
try:
# wait for onroad. timeout assumes panda is up to date
with Timeout(10, "timed out waiting to go onroad"):
@@ -28,15 +31,14 @@ def test_time_to_onroad():
# wait for engageability
try:
with Timeout(10, "timed out waiting for engageable"):
- sendcan_frame = None
+ initialized = False
while True:
sm.update(100)
- # sendcan is only sent once we're initialized
- if sm.seen['controlsState'] and sendcan_frame is None:
- sendcan_frame = sm.frame
+ if sm.seen['onroadEvents'] and not any(EventName.controlsInitializing == e.name for e in sm['onroadEvents']):
+ initialized = True
- if sendcan_frame is not None and sm.recv_frame['sendcan'] > sendcan_frame:
+ if initialized:
sm.update(100)
assert sm['controlsState'].engageable, f"events: {sm['onroadEvents']}"
break
diff --git a/selfdrive/test/test_updated.py b/selfdrive/test/test_updated.py
index dd79e03de4..12619d4246 100755
--- a/selfdrive/test/test_updated.py
+++ b/selfdrive/test/test_updated.py
@@ -4,7 +4,6 @@ import os
import pytest
import time
import tempfile
-import unittest
import shutil
import signal
import subprocess
@@ -15,9 +14,9 @@ from openpilot.common.params import Params
@pytest.mark.tici
-class TestUpdated(unittest.TestCase):
+class TestUpdated:
- def setUp(self):
+ def setup_method(self):
self.updated_proc = None
self.tmp_dir = tempfile.TemporaryDirectory()
@@ -59,7 +58,7 @@ class TestUpdated(unittest.TestCase):
self.params.clear_all()
os.sync()
- def tearDown(self):
+ def teardown_method(self):
try:
if self.updated_proc is not None:
self.updated_proc.terminate()
@@ -90,7 +89,7 @@ class TestUpdated(unittest.TestCase):
os.environ["UPDATER_STAGING_ROOT"] = self.staging_dir
os.environ["UPDATER_NEOS_VERSION"] = self.neos_version
os.environ["UPDATER_NEOSUPDATE_DIR"] = self.neosupdate_dir
- updated_path = os.path.join(self.basedir, "selfdrive/updated.py")
+ updated_path = os.path.join(self.basedir, "system/updated.py")
return subprocess.Popen(updated_path, env=os.environ)
def _start_updater(self, offroad=True, nosleep=False):
@@ -167,7 +166,7 @@ class TestUpdated(unittest.TestCase):
t = self._read_param("LastUpdateTime")
last_update_time = datetime.datetime.fromisoformat(t)
td = datetime.datetime.utcnow() - last_update_time
- self.assertLess(td.total_seconds(), 10)
+ assert td.total_seconds() < 10
self.params.remove("LastUpdateTime")
# wait a bit for the rest of the params to be written
@@ -175,13 +174,13 @@ class TestUpdated(unittest.TestCase):
# check params
update = self._read_param("UpdateAvailable")
- self.assertEqual(update == "1", update_available, f"UpdateAvailable: {repr(update)}")
- self.assertEqual(self._read_param("UpdateFailedCount"), "0")
+ assert update == "1" == update_available, f"UpdateAvailable: {repr(update)}"
+ assert self._read_param("UpdateFailedCount") == "0"
# TODO: check that the finalized update actually matches remote
# check the .overlay_init and .overlay_consistent flags
- self.assertTrue(os.path.isfile(os.path.join(self.basedir, ".overlay_init")))
- self.assertEqual(os.path.isfile(os.path.join(self.finalized_dir, ".overlay_consistent")), update_available)
+ assert os.path.isfile(os.path.join(self.basedir, ".overlay_init"))
+ assert os.path.isfile(os.path.join(self.finalized_dir, ".overlay_consistent")) == update_available
# *** test cases ***
@@ -214,7 +213,7 @@ class TestUpdated(unittest.TestCase):
self._check_update_state(True)
# Let the updater run for 10 cycles, and write an update every cycle
- @unittest.skip("need to make this faster")
+ @pytest.mark.skip("need to make this faster")
def test_update_loop(self):
self._start_updater()
@@ -243,12 +242,12 @@ class TestUpdated(unittest.TestCase):
# run another cycle, should have a new mtime
self._wait_for_update(clear_param=True)
second_mtime = os.path.getmtime(overlay_init_fn)
- self.assertTrue(first_mtime != second_mtime)
+ assert first_mtime != second_mtime
# run another cycle, mtime should be same as last cycle
self._wait_for_update(clear_param=True)
new_mtime = os.path.getmtime(overlay_init_fn)
- self.assertTrue(second_mtime == new_mtime)
+ assert second_mtime == new_mtime
# Make sure updated exits if another instance is running
def test_multiple_instances(self):
@@ -260,7 +259,7 @@ class TestUpdated(unittest.TestCase):
# start another instance
second_updated = self._get_updated_proc()
ret_code = second_updated.wait(timeout=5)
- self.assertTrue(ret_code is not None)
+ assert ret_code is not None
# *** test cases with NEOS updates ***
@@ -277,10 +276,10 @@ class TestUpdated(unittest.TestCase):
self._start_updater()
self._wait_for_update(clear_param=True)
self._check_update_state(False)
- self.assertFalse(os.path.isdir(self.neosupdate_dir))
+ assert not os.path.isdir(self.neosupdate_dir)
# Let the updater run with no update for a cycle, then write an update
- @unittest.skip("TODO: only runs on device")
+ @pytest.mark.skip("TODO: only runs on device")
def test_update_with_neos_update(self):
# bump the NEOS version and commit it
self._run([
@@ -295,8 +294,4 @@ class TestUpdated(unittest.TestCase):
self._check_update_state(True)
# TODO: more comprehensive check
- self.assertTrue(os.path.isdir(self.neosupdate_dir))
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert os.path.isdir(self.neosupdate_dir)
diff --git a/selfdrive/test/test_valgrind_replay.py b/selfdrive/test/test_valgrind_replay.py
deleted file mode 100755
index 75520df91b..0000000000
--- a/selfdrive/test/test_valgrind_replay.py
+++ /dev/null
@@ -1,117 +0,0 @@
-#!/usr/bin/env python3
-import os
-import threading
-import time
-import unittest
-import subprocess
-import signal
-
-if "CI" in os.environ:
- def tqdm(x):
- return x
-else:
- from tqdm import tqdm # type: ignore
-
-import cereal.messaging as messaging
-from collections import namedtuple
-from openpilot.tools.lib.logreader import LogReader
-from openpilot.tools.lib.openpilotci import get_url
-from openpilot.common.basedir import BASEDIR
-
-ProcessConfig = namedtuple('ProcessConfig', ['proc_name', 'pub_sub', 'ignore', 'command', 'path', 'segment', 'wait_for_response'])
-
-CONFIGS = [
- ProcessConfig(
- proc_name="ubloxd",
- pub_sub={
- "ubloxRaw": ["ubloxGnss", "gpsLocationExternal"],
- },
- ignore=[],
- command="./ubloxd",
- path="system/ubloxd",
- segment="0375fdf7b1ce594d|2019-06-13--08-32-25--3",
- wait_for_response=True
- ),
-]
-
-
-class TestValgrind(unittest.TestCase):
- def extract_leak_sizes(self, log):
- if "All heap blocks were freed -- no leaks are possible" in log:
- return (0,0,0)
-
- log = log.replace(",","") # fixes casting to int issue with large leaks
- err_lost1 = log.split("definitely lost: ")[1]
- err_lost2 = log.split("indirectly lost: ")[1]
- err_lost3 = log.split("possibly lost: ")[1]
- definitely_lost = int(err_lost1.split(" ")[0])
- indirectly_lost = int(err_lost2.split(" ")[0])
- possibly_lost = int(err_lost3.split(" ")[0])
- return (definitely_lost, indirectly_lost, possibly_lost)
-
- def valgrindlauncher(self, arg, cwd):
- os.chdir(os.path.join(BASEDIR, cwd))
- # Run valgrind on a process
- command = "valgrind --leak-check=full " + arg
- p = subprocess.Popen(command, stderr=subprocess.PIPE, shell=True, preexec_fn=os.setsid)
-
- while not self.replay_done:
- time.sleep(0.1)
-
- # Kill valgrind and extract leak output
- os.killpg(os.getpgid(p.pid), signal.SIGINT)
- _, err = p.communicate()
- error_msg = str(err, encoding='utf-8')
- with open(os.path.join(BASEDIR, "selfdrive/test/valgrind_logs.txt"), "a") as f:
- f.write(error_msg)
- f.write(5 * "\n")
- definitely_lost, indirectly_lost, possibly_lost = self.extract_leak_sizes(error_msg)
- if max(definitely_lost, indirectly_lost, possibly_lost) > 0:
- self.leak = True
- print("LEAKS from", arg, "\nDefinitely lost:", definitely_lost, "\nIndirectly lost", indirectly_lost, "\nPossibly lost", possibly_lost)
- else:
- self.leak = False
-
- def replay_process(self, config, logreader):
- pub_sockets = list(config.pub_sub.keys()) # We dump data from logs here
- sub_sockets = [s for _, sub in config.pub_sub.items() for s in sub] # We get responses here
- pm = messaging.PubMaster(pub_sockets)
- sm = messaging.SubMaster(sub_sockets)
-
- print("Sorting logs")
- all_msgs = sorted(logreader, key=lambda msg: msg.logMonoTime)
- pub_msgs = [msg for msg in all_msgs if msg.which() in list(config.pub_sub.keys())]
-
- thread = threading.Thread(target=self.valgrindlauncher, args=(config.command, config.path))
- thread.daemon = True
- thread.start()
-
- while not all(pm.all_readers_updated(s) for s in config.pub_sub.keys()):
- time.sleep(0)
-
- for msg in tqdm(pub_msgs):
- pm.send(msg.which(), msg.as_builder())
- if config.wait_for_response:
- sm.update(100)
-
- self.replay_done = True
-
- def test_config(self):
- open(os.path.join(BASEDIR, "selfdrive/test/valgrind_logs.txt"), "w").close()
-
- for cfg in CONFIGS:
- self.leak = None
- self.replay_done = False
-
- r, n = cfg.segment.rsplit("--", 1)
- lr = LogReader(get_url(r, n))
- self.replay_process(cfg, lr)
-
- while self.leak is None:
- time.sleep(0.1) # Wait for the valgrind to finish
-
- self.assertFalse(self.leak)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/thermald/tests/test_fan_controller.py b/selfdrive/thermald/tests/test_fan_controller.py
deleted file mode 100755
index 7081e1353e..0000000000
--- a/selfdrive/thermald/tests/test_fan_controller.py
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/usr/bin/env python3
-import unittest
-from unittest.mock import Mock, patch
-from parameterized import parameterized
-
-from openpilot.selfdrive.thermald.fan_controller import TiciFanController
-
-ALL_CONTROLLERS = [(TiciFanController,)]
-
-def patched_controller(controller_class):
- with patch("os.system", new=Mock()):
- return controller_class()
-
-class TestFanController(unittest.TestCase):
- def wind_up(self, controller, ignition=True):
- for _ in range(1000):
- controller.update(100, ignition)
-
- def wind_down(self, controller, ignition=False):
- for _ in range(1000):
- controller.update(10, ignition)
-
- @parameterized.expand(ALL_CONTROLLERS)
- def test_hot_onroad(self, controller_class):
- controller = patched_controller(controller_class)
- self.wind_up(controller)
- self.assertGreaterEqual(controller.update(100, True), 70)
-
- @parameterized.expand(ALL_CONTROLLERS)
- def test_offroad_limits(self, controller_class):
- controller = patched_controller(controller_class)
- self.wind_up(controller)
- self.assertLessEqual(controller.update(100, False), 30)
-
- @parameterized.expand(ALL_CONTROLLERS)
- def test_no_fan_wear(self, controller_class):
- controller = patched_controller(controller_class)
- self.wind_down(controller)
- self.assertEqual(controller.update(10, False), 0)
-
- @parameterized.expand(ALL_CONTROLLERS)
- def test_limited(self, controller_class):
- controller = patched_controller(controller_class)
- self.wind_up(controller, True)
- self.assertEqual(controller.update(100, True), 100)
-
- @parameterized.expand(ALL_CONTROLLERS)
- def test_windup_speed(self, controller_class):
- controller = patched_controller(controller_class)
- self.wind_down(controller, True)
- for _ in range(10):
- controller.update(90, True)
- self.assertGreaterEqual(controller.update(90, True), 60)
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/thermald/tests/test_power_monitoring.py b/selfdrive/thermald/tests/test_power_monitoring.py
deleted file mode 100755
index c3a890f068..0000000000
--- a/selfdrive/thermald/tests/test_power_monitoring.py
+++ /dev/null
@@ -1,200 +0,0 @@
-#!/usr/bin/env python3
-import unittest
-from unittest.mock import patch
-
-from openpilot.common.params import Params
-from openpilot.selfdrive.thermald.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \
- CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S
-
-
-# Create fake time
-ssb = 0.
-def mock_time_monotonic():
- global ssb
- ssb += 1.
- return ssb
-
-TEST_DURATION_S = 50
-GOOD_VOLTAGE = 12 * 1e3
-VOLTAGE_BELOW_PAUSE_CHARGING = (VBATT_PAUSE_CHARGING - 1) * 1e3
-
-def pm_patch(name, value, constant=False):
- if constant:
- return patch(f"openpilot.selfdrive.thermald.power_monitoring.{name}", value)
- return patch(f"openpilot.selfdrive.thermald.power_monitoring.{name}", return_value=value)
-
-
-@patch("time.monotonic", new=mock_time_monotonic)
-class TestPowerMonitoring(unittest.TestCase):
- def setUp(self):
- self.params = Params()
-
- # Test to see that it doesn't do anything when pandaState is None
- def test_pandaState_present(self):
- pm = PowerMonitoring()
- for _ in range(10):
- pm.calculate(None, None)
- self.assertEqual(pm.get_power_used(), 0)
- self.assertEqual(pm.get_car_battery_capacity(), (CAR_BATTERY_CAPACITY_uWh / 10))
-
- # Test to see that it doesn't integrate offroad when ignition is True
- def test_offroad_ignition(self):
- pm = PowerMonitoring()
- for _ in range(10):
- pm.calculate(GOOD_VOLTAGE, True)
- self.assertEqual(pm.get_power_used(), 0)
-
- # Test to see that it integrates with discharging battery
- def test_offroad_integration_discharging(self):
- POWER_DRAW = 4
- with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
- pm = PowerMonitoring()
- for _ in range(TEST_DURATION_S + 1):
- pm.calculate(GOOD_VOLTAGE, False)
- expected_power_usage = ((TEST_DURATION_S/3600) * POWER_DRAW * 1e6)
- self.assertLess(abs(pm.get_power_used() - expected_power_usage), 10)
-
- # Test to check positive integration of car_battery_capacity
- def test_car_battery_integration_onroad(self):
- POWER_DRAW = 4
- with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
- pm = PowerMonitoring()
- pm.car_battery_capacity_uWh = 0
- for _ in range(TEST_DURATION_S + 1):
- pm.calculate(GOOD_VOLTAGE, True)
- expected_capacity = ((TEST_DURATION_S/3600) * CAR_CHARGING_RATE_W * 1e6)
- self.assertLess(abs(pm.get_car_battery_capacity() - expected_capacity), 10)
-
- # Test to check positive integration upper limit
- def test_car_battery_integration_upper_limit(self):
- POWER_DRAW = 4
- with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
- pm = PowerMonitoring()
- pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh - 1000
- for _ in range(TEST_DURATION_S + 1):
- pm.calculate(GOOD_VOLTAGE, True)
- estimated_capacity = CAR_BATTERY_CAPACITY_uWh + (CAR_CHARGING_RATE_W / 3600 * 1e6)
- self.assertLess(abs(pm.get_car_battery_capacity() - estimated_capacity), 10)
-
- # Test to check negative integration of car_battery_capacity
- def test_car_battery_integration_offroad(self):
- POWER_DRAW = 4
- with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
- pm = PowerMonitoring()
- pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
- for _ in range(TEST_DURATION_S + 1):
- pm.calculate(GOOD_VOLTAGE, False)
- expected_capacity = CAR_BATTERY_CAPACITY_uWh - ((TEST_DURATION_S/3600) * POWER_DRAW * 1e6)
- self.assertLess(abs(pm.get_car_battery_capacity() - expected_capacity), 10)
-
- # Test to check negative integration lower limit
- def test_car_battery_integration_lower_limit(self):
- POWER_DRAW = 4
- with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
- pm = PowerMonitoring()
- pm.car_battery_capacity_uWh = 1000
- for _ in range(TEST_DURATION_S + 1):
- pm.calculate(GOOD_VOLTAGE, False)
- estimated_capacity = 0 - ((1/3600) * POWER_DRAW * 1e6)
- self.assertLess(abs(pm.get_car_battery_capacity() - estimated_capacity), 10)
-
- # Test to check policy of stopping charging after MAX_TIME_OFFROAD_S
- def test_max_time_offroad(self):
- MOCKED_MAX_OFFROAD_TIME = 3600
- POWER_DRAW = 0 # To stop shutting down for other reasons
- with pm_patch("MAX_TIME_OFFROAD_S", MOCKED_MAX_OFFROAD_TIME, constant=True), pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
- pm = PowerMonitoring()
- pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
- start_time = ssb
- ignition = False
- while ssb <= start_time + MOCKED_MAX_OFFROAD_TIME:
- pm.calculate(GOOD_VOLTAGE, ignition)
- if (ssb - start_time) % 1000 == 0 and ssb < start_time + MOCKED_MAX_OFFROAD_TIME:
- self.assertFalse(pm.should_shutdown(ignition, True, start_time, False))
- self.assertTrue(pm.should_shutdown(ignition, True, start_time, False))
-
- def test_car_voltage(self):
- POWER_DRAW = 0 # To stop shutting down for other reasons
- TEST_TIME = 350
- VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S = 50
- with pm_patch("VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S", VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S, constant=True), \
- pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
- pm = PowerMonitoring()
- pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
- ignition = False
- start_time = ssb
- for i in range(TEST_TIME):
- pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
- if i % 10 == 0:
- self.assertEqual(pm.should_shutdown(ignition, True, start_time, True),
- (pm.car_voltage_mV < VBATT_PAUSE_CHARGING * 1e3 and
- (ssb - start_time) > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S and
- (ssb - start_time) > DELAY_SHUTDOWN_TIME_S))
- self.assertTrue(pm.should_shutdown(ignition, True, start_time, True))
-
- # Test to check policy of not stopping charging when DisablePowerDown is set
- def test_disable_power_down(self):
- POWER_DRAW = 0 # To stop shutting down for other reasons
- TEST_TIME = 100
- self.params.put_bool("DisablePowerDown", True)
- with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
- pm = PowerMonitoring()
- pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
- ignition = False
- for i in range(TEST_TIME):
- pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
- if i % 10 == 0:
- self.assertFalse(pm.should_shutdown(ignition, True, ssb, False))
- self.assertFalse(pm.should_shutdown(ignition, True, ssb, False))
-
- # Test to check policy of not stopping charging when ignition
- def test_ignition(self):
- POWER_DRAW = 0 # To stop shutting down for other reasons
- TEST_TIME = 100
- with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
- pm = PowerMonitoring()
- pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
- ignition = True
- for i in range(TEST_TIME):
- pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
- if i % 10 == 0:
- self.assertFalse(pm.should_shutdown(ignition, True, ssb, False))
- self.assertFalse(pm.should_shutdown(ignition, True, ssb, False))
-
- # Test to check policy of not stopping charging when harness is not connected
- def test_harness_connection(self):
- POWER_DRAW = 0 # To stop shutting down for other reasons
- TEST_TIME = 100
- with pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW):
- pm = PowerMonitoring()
- pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
-
- ignition = False
- for i in range(TEST_TIME):
- pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
- if i % 10 == 0:
- self.assertFalse(pm.should_shutdown(ignition, False, ssb, False))
- self.assertFalse(pm.should_shutdown(ignition, False, ssb, False))
-
- def test_delay_shutdown_time(self):
- pm = PowerMonitoring()
- pm.car_battery_capacity_uWh = 0
- ignition = False
- in_car = True
- offroad_timestamp = ssb
- started_seen = True
- pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
-
- while ssb < offroad_timestamp + DELAY_SHUTDOWN_TIME_S:
- self.assertFalse(pm.should_shutdown(ignition, in_car,
- offroad_timestamp,
- started_seen),
- f"Should not shutdown before {DELAY_SHUTDOWN_TIME_S} seconds offroad time")
- self.assertTrue(pm.should_shutdown(ignition, in_car,
- offroad_timestamp,
- started_seen),
- f"Should shutdown after {DELAY_SHUTDOWN_TIME_S} seconds offroad time")
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/ui/qt/api.cc b/selfdrive/ui/qt/api.cc
index 0e321d4e10..6889b40e51 100644
--- a/selfdrive/ui/qt/api.cc
+++ b/selfdrive/ui/qt/api.cc
@@ -1,6 +1,5 @@
#include "selfdrive/ui/qt/api.h"
-#include
#include
#include
@@ -8,38 +7,41 @@
#include
#include
#include
-#include
#include
#include
+#include
#include
-#include "common/params.h"
#include "common/util.h"
#include "system/hardware/hw.h"
#include "selfdrive/ui/qt/util.h"
namespace CommaApi {
-QByteArray rsa_sign(const QByteArray &data) {
- static std::string key = util::read_file(Path::rsa_file());
- if (key.empty()) {
- qDebug() << "No RSA private key found, please run manager.py or registration.py";
- return {};
+RSA *get_rsa_private_key() {
+ static std::unique_ptr rsa_private(nullptr, RSA_free);
+ if (!rsa_private) {
+ FILE *fp = fopen(Path::rsa_file().c_str(), "rb");
+ if (!fp) {
+ qDebug() << "No RSA private key found, please run manager.py or registration.py";
+ return nullptr;
+ }
+ rsa_private.reset(PEM_read_RSAPrivateKey(fp, NULL, NULL, NULL));
+ fclose(fp);
}
+ return rsa_private.get();
+}
- BIO* mem = BIO_new_mem_buf(key.data(), key.size());
- assert(mem);
- RSA* rsa_private = PEM_read_bio_RSAPrivateKey(mem, NULL, NULL, NULL);
- assert(rsa_private);
- auto sig = QByteArray();
- sig.resize(RSA_size(rsa_private));
+QByteArray rsa_sign(const QByteArray &data) {
+ RSA *rsa_private = get_rsa_private_key();
+ if (!rsa_private) return {};
+
+ QByteArray sig(RSA_size(rsa_private), Qt::Uninitialized);
unsigned int sig_len;
int ret = RSA_sign(NID_sha256, (unsigned char*)data.data(), data.size(), (unsigned char*)sig.data(), &sig_len, rsa_private);
assert(ret == 1);
- assert(sig_len == sig.size());
- BIO_free(mem);
- RSA_free(rsa_private);
+ assert(sig.size() == sig_len);
return sig;
}
@@ -57,9 +59,7 @@ QString create_jwt(const QJsonObject &payloads, int expiry) {
QJsonDocument(payload).toJson(QJsonDocument::Compact).toBase64(b64_opts);
auto hash = QCryptographicHash::hash(jwt.toUtf8(), QCryptographicHash::Sha256);
- auto sig = rsa_sign(hash);
- jwt += '.' + sig.toBase64(b64_opts);
- return jwt;
+ return jwt + "." + rsa_sign(hash).toBase64(b64_opts);
}
} // namespace CommaApi
diff --git a/selfdrive/ui/qt/maps/map.cc b/selfdrive/ui/qt/maps/map.cc
index 5f178e9f8f..490eb118ca 100644
--- a/selfdrive/ui/qt/maps/map.cc
+++ b/selfdrive/ui/qt/maps/map.cc
@@ -5,6 +5,7 @@
#include
+#include "common/swaglog.h"
#include "selfdrive/ui/qt/maps/map_helpers.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/ui.h"
@@ -118,6 +119,14 @@ void MapWindow::updateState(const UIState &s) {
const SubMaster &sm = *(s.sm);
update();
+ // on rising edge of a valid system time, reinitialize the map to set a new token
+ if (sm.valid("clocks") && !prev_time_valid) {
+ LOGW("Time is now valid, reinitializing map");
+ m_settings.setApiKey(get_mapbox_token());
+ initializeGL();
+ }
+ prev_time_valid = sm.valid("clocks");
+
if (sm.updated("liveLocationKalman")) {
auto locationd_location = sm["liveLocationKalman"].getLiveLocationKalman();
auto locationd_pos = locationd_location.getPositionGeodetic();
@@ -262,6 +271,10 @@ void MapWindow::initializeGL() {
loaded_once = true;
}
});
+
+ QObject::connect(m_map.data(), &QMapLibre::Map::mapLoadingFailed, [=](QMapLibre::Map::MapLoadingFailure err_code, const QString &reason) {
+ LOGE("Map loading failed with %d: '%s'\n", err_code, reason.toStdString().c_str());
+ });
}
void MapWindow::paintGL() {
diff --git a/selfdrive/ui/qt/maps/map.h b/selfdrive/ui/qt/maps/map.h
index 8193d55729..31a44f27b1 100644
--- a/selfdrive/ui/qt/maps/map.h
+++ b/selfdrive/ui/qt/maps/map.h
@@ -51,6 +51,7 @@ private:
void setError(const QString &err_str);
bool loaded_once = false;
+ bool prev_time_valid = true;
// Panning
QPointF m_lastPos;
diff --git a/selfdrive/ui/qt/offroad/software_settings.cc b/selfdrive/ui/qt/offroad/software_settings.cc
index d12db3f878..c8245205ce 100644
--- a/selfdrive/ui/qt/offroad/software_settings.cc
+++ b/selfdrive/ui/qt/offroad/software_settings.cc
@@ -17,7 +17,7 @@
void SoftwarePanel::checkForUpdates() {
- std::system("pkill -SIGUSR1 -f selfdrive.updated.updated");
+ std::system("pkill -SIGUSR1 -f system.updated.updated");
}
SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) {
@@ -36,7 +36,7 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) {
if (downloadBtn->text() == tr("CHECK")) {
checkForUpdates();
} else {
- std::system("pkill -SIGHUP -f selfdrive.updated.updated");
+ std::system("pkill -SIGHUP -f system.updated.updated");
}
});
addItem(downloadBtn);
diff --git a/selfdrive/ui/tests/test_soundd.py b/selfdrive/ui/tests/test_soundd.py
index 94ce26eb47..d15a6c1831 100755
--- a/selfdrive/ui/tests/test_soundd.py
+++ b/selfdrive/ui/tests/test_soundd.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
-import unittest
from cereal import car
from cereal import messaging
@@ -11,7 +10,7 @@ import time
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
-class TestSoundd(unittest.TestCase):
+class TestSoundd:
def test_check_controls_timeout_alert(self):
sm = SubMaster(['controlsState'])
pm = PubMaster(['controlsState'])
@@ -26,16 +25,13 @@ class TestSoundd(unittest.TestCase):
sm.update(0)
- self.assertFalse(check_controls_timeout_alert(sm))
+ assert not check_controls_timeout_alert(sm)
for _ in range(CONTROLS_TIMEOUT * 110):
sm.update(0)
time.sleep(0.01)
- self.assertTrue(check_controls_timeout_alert(sm))
+ assert check_controls_timeout_alert(sm)
# TODO: add test with micd for checking that soundd actually outputs sounds
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py
index 8e50695e70..57de069d0b 100755
--- a/selfdrive/ui/tests/test_translations.py
+++ b/selfdrive/ui/tests/test_translations.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
+import pytest
import json
import os
import re
-import unittest
import shutil
import tempfile
import xml.etree.ElementTree as ET
@@ -21,7 +21,7 @@ FORMAT_ARG = re.compile("%[0-9]+")
@parameterized_class(("name", "file"), translation_files.items())
-class TestTranslations(unittest.TestCase):
+class TestTranslations:
name: str
file: str
@@ -32,8 +32,8 @@ class TestTranslations(unittest.TestCase):
return f.read()
def test_missing_translation_files(self):
- self.assertTrue(os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts")),
- f"{self.name} has no XML translation file, run selfdrive/ui/update_translations.py")
+ assert os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts")), \
+ f"{self.name} has no XML translation file, run selfdrive/ui/update_translations.py"
def test_translations_updated(self):
with tempfile.TemporaryDirectory() as tmpdir:
@@ -42,19 +42,19 @@ class TestTranslations(unittest.TestCase):
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
new_translations = self._read_translation_file(tmpdir, self.file)
- self.assertEqual(cur_translations, new_translations,
- f"{self.file} ({self.name}) XML translation file out of date. Run selfdrive/ui/update_translations.py to update the translation files")
+ assert cur_translations == new_translations, \
+ f"{self.file} ({self.name}) XML translation file out of date. Run selfdrive/ui/update_translations.py to update the translation files"
- @unittest.skip("Only test unfinished translations before going to release")
+ @pytest.mark.skip("Only test unfinished translations before going to release")
def test_unfinished_translations(self):
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
- self.assertTrue(UNFINISHED_TRANSLATION_TAG not in cur_translations,
- f"{self.file} ({self.name}) translation file has unfinished translations. Finish translations or mark them as completed in Qt Linguist")
+ assert UNFINISHED_TRANSLATION_TAG not in cur_translations, \
+ f"{self.file} ({self.name}) translation file has unfinished translations. Finish translations or mark them as completed in Qt Linguist"
def test_vanished_translations(self):
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
- self.assertTrue("" not in cur_translations,
- f"{self.file} ({self.name}) translation file has obsolete translations. Run selfdrive/ui/update_translations.py --vanish to remove them")
+ assert "" not in cur_translations, \
+ f"{self.file} ({self.name}) translation file has obsolete translations. Run selfdrive/ui/update_translations.py --vanish to remove them"
def test_finished_translations(self):
"""
@@ -81,27 +81,27 @@ class TestTranslations(unittest.TestCase):
numerusform = [t.text for t in translation.findall("numerusform")]
for nf in numerusform:
- self.assertIsNotNone(nf, f"Ensure all plural translation forms are completed: {source_text}")
- self.assertIn("%n", nf, "Ensure numerus argument (%n) exists in translation.")
- self.assertIsNone(FORMAT_ARG.search(nf), f"Plural translations must use %n, not %1, %2, etc.: {numerusform}")
+ assert nf is not None, f"Ensure all plural translation forms are completed: {source_text}"
+ assert "%n" in nf, "Ensure numerus argument (%n) exists in translation."
+ assert FORMAT_ARG.search(nf) is None, f"Plural translations must use %n, not %1, %2, etc.: {numerusform}"
else:
- self.assertIsNotNone(translation.text, f"Ensure translation is completed: {source_text}")
+ assert translation.text is not None, f"Ensure translation is completed: {source_text}"
source_args = FORMAT_ARG.findall(source_text)
translation_args = FORMAT_ARG.findall(translation.text)
- self.assertEqual(sorted(source_args), sorted(translation_args),
- f"Ensure format arguments are consistent: `{source_text}` vs. `{translation.text}`")
+ assert sorted(source_args) == sorted(translation_args), \
+ f"Ensure format arguments are consistent: `{source_text}` vs. `{translation.text}`"
def test_no_locations(self):
for line in self._read_translation_file(TRANSLATIONS_DIR, self.file).splitlines():
- self.assertFalse(line.strip().startswith(LOCATION_TAG),
- f"Line contains location tag: {line.strip()}, remove all line numbers.")
+ assert not line.strip().startswith(LOCATION_TAG), \
+ f"Line contains location tag: {line.strip()}, remove all line numbers."
def test_entities_error(self):
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
matches = re.findall(r'@(\w+);', cur_translations)
- self.assertEqual(len(matches), 0, f"The string(s) {matches} were found with '@' instead of '&'")
+ assert len(matches) == 0, f"The string(s) {matches} were found with '@' instead of '&'"
def test_bad_language(self):
IGNORED_WORDS = {'pédale'}
@@ -128,7 +128,3 @@ class TestTranslations(unittest.TestCase):
words = set(translation_text.translate(str.maketrans('', '', string.punctuation + '%n')).lower().split())
bad_words_found = words & (banned_words - IGNORED_WORDS)
assert not bad_words_found, f"Bad language found in {self.name}: '{translation_text}'. Bad word(s): {', '.join(bad_words_found)}"
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py
index c834107780..871333563d 100644
--- a/selfdrive/ui/tests/test_ui/run.py
+++ b/selfdrive/ui/tests/test_ui/run.py
@@ -8,9 +8,7 @@ import numpy as np
import os
import pywinctl
import time
-import unittest
-from parameterized import parameterized
from cereal import messaging, car, log
from cereal.visionipc import VisionIpcServer, VisionStreamType
@@ -122,16 +120,11 @@ TEST_OUTPUT_DIR = TEST_DIR / "report"
SCREENSHOTS_DIR = TEST_OUTPUT_DIR / "screenshots"
-class TestUI(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
+class TestUI:
+ def __init__(self):
os.environ["SCALE"] = "1"
sys.modules["mouseinfo"] = False
- @classmethod
- def tearDownClass(cls):
- del sys.modules["mouseinfo"]
-
def setup(self):
self.sm = SubMaster(["uiDebug"])
self.pm = PubMaster(["deviceState", "pandaStates", "controlsState", 'roadCameraState', 'wideRoadCameraState', 'liveLocationKalman'])
@@ -147,8 +140,8 @@ class TestUI(unittest.TestCase):
def screenshot(self):
import pyautogui
im = pyautogui.screenshot(region=(self.ui.left, self.ui.top, self.ui.width, self.ui.height))
- self.assertEqual(im.width, 2160)
- self.assertEqual(im.height, 1080)
+ assert im.width == 2160
+ assert im.height == 1080
img = np.array(im)
im.close()
return img
@@ -158,7 +151,6 @@ class TestUI(unittest.TestCase):
pyautogui.click(self.ui.left + x, self.ui.top + y, *args, **kwargs)
time.sleep(UI_DELAY) # give enough time for the UI to react
- @parameterized.expand(CASES.items())
@with_processes(["ui"])
def test_ui(self, name, setup_case):
self.setup()
@@ -188,7 +180,10 @@ def create_screenshots():
shutil.rmtree(TEST_OUTPUT_DIR)
SCREENSHOTS_DIR.mkdir(parents=True)
- unittest.main(exit=False)
+
+ t = TestUI()
+ for name, setup in CASES.items():
+ t.test_ui(name, setup)
if __name__ == "__main__":
print("creating test screenshots")
diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts
index e648298cef..b8b7af68c6 100644
--- a/selfdrive/ui/translations/main_ko.ts
+++ b/selfdrive/ui/translations/main_ko.ts
@@ -1502,7 +1502,7 @@ This may take up to a minute.
Sidebar
CONNECT
- 연결됨
+ 커넥트
OFFLINE
diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts
index 29f3e88763..3c4412bfe4 100644
--- a/selfdrive/ui/translations/main_zh-CHS.ts
+++ b/selfdrive/ui/translations/main_zh-CHS.ts
@@ -72,11 +72,11 @@
CONNECT
- 连接
+ 连线
Enter SSID
- 输入SSID
+ 输入 SSID
Enter password
@@ -449,11 +449,11 @@ Please use caution when using this feature. Only use the blinker when traffic an
Review the rules, features, and limitations of sunnypilot
- 回顾sunnypilot的功能、使用说明和注意事项
+ 查看 sunnypilot 的使用规则,以及其功能和限制
sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required.
- sunnypilot要求设备安装偏航角4°范围内,俯仰角上5°或下9°范围内。sunnypilot是连续校准,很少需要重置。
+ sunnypilot要求设备安装的偏航角在左4°和右4°之间,俯仰角在上5°和下9°之间。一般来说,openpilot会持续更新校准,很少需要重置。
OFF
@@ -465,11 +465,11 @@ Please use caution when using this feature. Only use the blinker when traffic an
Pair Device
- 绑定设备
+ 配对设备
PAIR
- 绑定
+ 配对
Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.
@@ -897,23 +897,23 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.OnroadAlerts
openpilot Unavailable
- openpilot无法使用
+ 无法使用 openpilot
Waiting for controls to start
- 正在等待控件启动
+ 等待控制服务啟動
TAKE CONTROL IMMEDIATELY
- 请立即接管
+ 立即接管
Controls Unresponsive
- 控制无响应
+ 控制服务无响应
Reboot Device
- 请重启设备
+ 重启设备
@@ -1280,7 +1280,7 @@ This may take up to a minute.
System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot.
- 系统重置已触发。按confirm可擦除所有内容和设置。按“取消”继续引导系统启动。
+ 系统重置已触发。按下“确认”以清除所有内容和设置,按下“取消”以继续启动。
@@ -1370,19 +1370,19 @@ This may take up to a minute.
Device
- 设备管理
+ 设备
Network
- 网络设置
+ 网络
Toggles
- 功能设定
+ 设定
Software
- 软件更新
+ 软件
sunnylink
@@ -1501,15 +1501,15 @@ This may take up to a minute.
Choose Software to Install
- 选择软件安装
+ 选择要安装的软件
openpilot
- openpilot官方版
+ openpilot
Custom Software
- openpilot定制版
+ 定制软件
@@ -2617,7 +2617,7 @@ This may take up to a minute.
Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off.
- 使用sunnypilot系统进行自适应巡航控制和车道保持辅助。要使用此功能,您必须时刻注意道路安全。重启启动设备才能生效。
+ 使用sunnypilot进行自适应巡航和车道保持辅助。使用此功能时您必须时刻保持注意力。该设置的更改在熄火时生效。
Custom Stock Longitudinal Control
@@ -2663,7 +2663,7 @@ This feature must be used along with SLC, and/or V-TSC, and/or M-TSC.
Enable driver monitoring even when openpilot is not engaged.
- 始终进行驾驶员监控。
+ 即使在openpilot未激活时也启用驾驶员监控。
Stock is recommended. In aggressive/maniac mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button.
diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts
index aa4eaa0c78..9d1c16db9f 100644
--- a/selfdrive/ui/translations/main_zh-CHT.ts
+++ b/selfdrive/ui/translations/main_zh-CHT.ts
@@ -86,18 +86,6 @@
for "%1"
給 "%1"
-
- Retain hotspot/tethering state
-
-
-
- Enabling this toggle will retain the hotspot/tethering toggle state across reboots.
-
-
-
- Ngrok Service
-
-
AnnotatedCameraWidget
@@ -122,91 +110,6 @@
速限
-
- AutoLaneChangeTimer
-
- s
-
-
-
- Nudge
-
-
-
- Nudgeless
-
-
-
- Auto Lane Change by Blinker
-
-
-
- Off
-
-
-
- Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge.
-Please use caution when using this feature. Only use the blinker when traffic and road conditions permit.
-
-
-
-
- BackupSettings
-
- Settings backed up for sunnylink Device ID:
-
-
-
- Settings updated successfully, but no additional data was returned by the server.
-
-
-
- OOPS! We made a booboo.
-
-
-
- Please try again later.
-
-
-
- Settings restored. Confirm to restart the interface.
-
-
-
- No settings found to restore.
-
-
-
-
- BrightnessControl
-
- Brightness
-
-
-
- Manually adjusts the global brightness of the screen.
-
-
-
- Auto
-
-
-
-
- CameraOffset
-
- Camera Offset - Laneful Only
-
-
-
- Hack to trick vehicle to be left or right biased in its lane. Decreasing the value will make the car keep more left, increasing will make it keep more right. Changes take effect immediately. Default: +4 cm
-
-
-
- cm
-
-
-
ConfirmationDialog
@@ -218,13 +121,6 @@ Please use caution when using this feature. Only use the blinker when traffic an
取消
-
- CustomOffsetsSettings
-
- Back
- 回上頁
-
-
DeclinePage
@@ -315,7 +211,7 @@ Please use caution when using this feature. Only use the blinker when traffic an
Review the rules, features, and limitations of openpilot
- 觀看 openpilot 的使用規則、功能和限制
+ 觀看 openpilot 的使用規則、功能和限制
Are you sure you want to review the training guide?
@@ -351,11 +247,11 @@ Please use caution when using this feature. Only use the blinker when traffic an
openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required.
- openpilot 需要將設備固定在左右偏差 4° 以內,朝上偏差 5° 以內或朝下偏差 9° 以內。鏡頭在後台會持續自動校準,很少有需要重設的情況。
+ openpilot 需要將裝置固定在左右偏差 4° 以內,朝上偏差 5° 以內或朝下偏差 9° 以內。鏡頭在後台會持續自動校準,很少有需要重置的情況。
Your device is pointed %1° %2 and %3° %4.
- 你的設備目前朝%2 %1° 以及朝%4 %3° 。
+ 你的裝置目前朝%2 %1° 以及朝%4 %3° 。
down
@@ -399,161 +295,15 @@ Please use caution when using this feature. Only use the blinker when traffic an
Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.
- 將您的設備與 comma connect (connect.comma.ai) 配對並領取您的 comma 高級會員優惠。
+ 將您的裝置與 comma connect (connect.comma.ai) 配對並領取您的 comma 高級會員優惠。
Pair Device
-
+ 配對裝置
PAIR
-
-
-
- TOGGLE
-
-
-
- Enable or disable PIN requirement for Fleet Manager access.
-
-
-
- Are you sure you want to turn off PIN requirement?
-
-
-
- Turn Off
-
-
-
- Error Troubleshoot
-
-
-
- Display error from the tmux session when an error has occurred from a system process.
-
-
-
- Reset Access Tokens for Map Services
-
-
-
- Reset self-service access tokens for Mapbox, Amap, and Google Maps.
-
-
-
- Are you sure you want to reset access tokens for all map services?
-
-
-
- Reset sunnypilot Settings
-
-
-
- Are you sure you want to reset all sunnypilot settings?
-
-
-
- Review the rules, features, and limitations of sunnypilot
-
-
-
- OFF
-
-
-
- sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required.
-
-
-
- Fleet Manager PIN:
-
-
-
- Toggle Onroad/Offroad
-
-
-
- Are you sure you want to unforce offroad?
-
-
-
- Unforce
-
-
-
- Are you sure you want to force offroad?
-
-
-
- Force
-
-
-
- Disengage to Force Offroad
-
-
-
- Unforce Offroad
-
-
-
- Force Offroad
-
-
-
-
- DisplayPanel
-
- Driving Screen Off: Non-Critical Events
-
-
-
- When <b>Driving Screen Off Timer</b> is not set to <b>"Always On"</b>:
-
-
-
- Enabled: Wake the brightness of the screen to display all events.
-
-
-
- Disabled: Wake the brightness of the screen to display critical events.
-
-
-
- Enable Screen Recorder
-
-
-
- Enable this will display a button on the onroad screen to toggle on or off real-time screen recording with UI elements.
-
-
-
-
- DriveStats
-
- Drives
-
-
-
- Hours
-
-
-
- ALL TIME
-
-
-
- PAST WEEK
-
-
-
- KM
-
-
-
- Miles
-
+ 配對
@@ -594,79 +344,6 @@ Please use caution when using this feature. Only use the blinker when traffic an
安裝中…
-
- LaneChangeSettings
-
- Back
- 回上頁
-
-
- Pause Lateral Below Speed w/ Blinker
-
-
-
- Enable this toggle to pause lateral actuation with blinker when traveling below 20 MPH or 32 km/h.
-
-
-
- Auto Lane Change: Delay with Blind Spot
-
-
-
- Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering.
-
-
-
- Block Lane Change: Road Edge Detection
-
-
-
- Enable this toggle to block lane change when road edge is detected on the stalk actuated side.
-
-
-
-
- MadsSettings
-
- Enable ACC+MADS with RES+/SET-
-
-
-
- Engage both M.A.D.S. and ACC with a single press of RES+ or SET- button.
-
-
-
- Note: Once M.A.D.S. is engaged via this mode, it will remain engaged until it is manually disabled via the M.A.D.S. button or car shut off.
-
-
-
- Toggle M.A.D.S. with Cruise Main
-
-
-
- Allows M.A.D.S. engagement/disengagement with "Cruise Main" cruise control button from the steering wheel.
-
-
-
- Remain Active
-
-
-
- Pause Steering
-
-
-
- Steering Mode After Braking
-
-
-
- Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot.
-
-Remain Active: ALC will remain active even after the brake pedal is pressed.
-Pause Steering: ALC will be paused after the brake pedal is manually pressed.
-
-
-
MapETA
@@ -708,48 +385,6 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.等待路線
-
- MaxTimeOffroad
-
- Max Time Offroad
-
-
-
- Device is automatically turned off after a set time when the engine is turned off (off-road) after driving (on-road).
-
-
-
- s
-
-
-
- m
- m
-
-
- hr
- 小時
-
-
- Always On
-
-
-
- Immediate
-
-
-
-
- MonitoringPanel
-
- Enable Hands on Wheel Monitoring
-
-
-
- Monitor and alert when driver is not keeping the hands on the steering wheel.
-
-
-
MultiOptionDialog
@@ -779,14 +414,6 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.Wrong password
密碼錯誤
-
- Scan
-
-
-
- Scanning...
-
-
OffroadAlert
@@ -810,11 +437,11 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.
An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install.
- 一個給您設備的操作系統的更新正在後台下載中。當更新準備好安裝時,您將收到提示進行更新。
+ 一個有關操作系統的更新正在後台下載中。當更新準備好安裝時,您將收到提示進行更新。
Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support.
- 設備註冊失敗。它將無法連接或上傳至 comma.ai 伺服器,並且無法獲得 comma.ai 的支援。如果這是一個官方設備,請訪問 https://comma.ai/support 。
+ 裝置註冊失敗。它將無法連接或上傳至 comma.ai 伺服器,並且無法獲得 comma.ai 的支援。如果這是一個官方裝置,請訪問 https://comma.ai/support 。
NVMe drive not mounted.
@@ -822,7 +449,7 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.
Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe.
- 檢測到不支援的 NVMe 固態硬碟。您的設備因為使用了不支援的 NVMe 固態硬碟可能會消耗更多電力並更易過熱。
+ 檢測到不支援的 NVMe 固態硬碟。您的裝置因為使用了不支援的 NVMe 固態硬碟可能會消耗更多電力並更易過熱。
openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.
@@ -834,21 +461,11 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.
openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.
- openpilot 偵測到設備的安裝位置發生變化。請確保設備完全安裝在支架上,並確保支架牢固地固定在擋風玻璃上。
+ openpilot 偵測到裝置的安裝位置發生變化。請確保裝置完全安裝在支架上,並確保支架牢固地固定在擋風玻璃上。
Device temperature too high. System cooling down before starting. Current internal component temperature: %1
- 設備溫度過高。系統正在冷卻中,等冷卻完畢後才會啟動。目前內部組件溫度:%1
-
-
- OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.
-
-%1
-
-
-
- sunnypilot is now in Forced Offroad mode. sunnypilot won't start until Forced Offroad mode is disabled. Go to "Settings" -> "Device" -> "Unforce Offroad" to exit Force Offroad mode.
-
+ 裝置溫度過高。系統正在冷卻中,等冷卻完畢後才會啟動。目前內部組件溫度:%1
@@ -870,210 +487,30 @@ Pause Steering: ALC will be paused after the brake pedal is manually pressed.OnroadAlerts
openpilot Unavailable
-
+ 無法使用 openpilot
Waiting for controls to start
-
+ 等待操控服務開始
TAKE CONTROL IMMEDIATELY
-
+ 立即接管
Controls Unresponsive
-
+ 操控服務沒有反應
Reboot Device
-
-
-
-
- OnroadScreenOff
-
- Driving Screen Off Timer
-
-
-
- Turn off the device screen or reduce brightness to protect the screen after driving starts. It automatically brightens or turns on when a touch or event occurs.
-
-
-
- s
-
-
-
- min
- 分鐘
-
-
- Always On
-
-
-
-
- OnroadScreenOffBrightness
-
- Driving Screen Off Brightness (%)
-
-
-
- When using the Driving Screen Off feature, the brightness is reduced according to the automatic brightness ratio.
-
-
-
- Dark
-
-
-
-
- OnroadSettings
-
- ONROAD OPTIONS
-
-
-
- <b>ONROAD SETTINGS | SUNNYPILOT</b>
-
-
-
-
- OsmPanel
-
- Mapd Version
-
-
-
- Offline Maps ETA
-
-
-
- Time Elapsed
-
-
-
- Downloaded Maps
-
-
-
- DELETE
-
-
-
- This will delete ALL downloaded maps
-
-Are you sure you want to delete all the maps?
-
-
-
- Yes, delete all the maps.
-
-
-
- Database Update
-
-
-
- CHECK
- 檢查
-
-
- Country
-
-
-
- SELECT
- 選取
-
-
- Fetching Country list...
-
-
-
- State
-
-
-
- Fetching State list...
-
-
-
- All
-
-
-
- REFRESH
-
-
-
- UPDATE
- 更新
-
-
- Download starting...
-
-
-
- Error: Invalid download. Retry.
-
-
-
- Download complete!
-
-
-
-
-
-Warning: You are on a metered connection!
-
-
-
- This will start the download process and it might take a while to complete.
-
-
-
- Continue on Metered
-
-
-
- Start Download
-
-
-
- m
-
-
-
- s
-
-
-
- Calculating...
-
-
-
- Downloaded
-
-
-
- Calculating ETA...
-
-
-
- Ready
-
-
-
- Time remaining:
-
+ 請重新啟裝置
PairingPopup
Pair your device to your comma account
- 將設備與您的 comma 帳號配對
+ 將裝置與您的 comma 帳號配對
Go to https://connect.comma.ai on your phone
@@ -1099,21 +536,6 @@ Warning: You are on a metered connection!
啟用
-
- PathOffset
-
- Path Offset
-
-
-
- Hack to trick the model path to be left or right biased of the lane. Decreasing the value will shift the model more left, increasing will shift the model more right. Changes take effect immediately.
-
-
-
- cm
-
-
-
PrimeAdWidget
@@ -1168,7 +590,7 @@ Warning: You are on a metered connection!
openpilot
- openpilot
+ openpilot
%n minute(s) ago
@@ -1206,11 +628,7 @@ Warning: You are on a metered connection!
now
-
-
-
- sunnypilot
-
+ 現在
@@ -1221,7 +639,7 @@ Warning: You are on a metered connection!
Are you sure you want to reset your device?
- 您確定要重設你的設備嗎?
+ 您確定要重設你的裝置嗎?
System Reset
@@ -1241,12 +659,12 @@ Warning: You are on a metered connection!
Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device.
- 無法掛載資料分割區。分割區可能已經毀損。請確認是否要刪除並重新設定。
+ 無法掛載資料分割區。分割區可能已經毀損。請確認是否要刪除並重置。
Resetting device...
This may take up to a minute.
- 設備重設中…
+ 重置中…
這可能需要一分鐘的時間。
@@ -1254,85 +672,6 @@ This may take up to a minute.
系統重設已啟動。按下「確認」以清除所有內容和設定,或按下「取消」以繼續開機。
-
- SPVehiclesTogglesPanel
-
- Hyundai/Kia/Genesis
-
-
-
- HKG CAN: Smoother Stopping Performance (Beta)
-
-
-
- Smoother stopping behind a stopped car or desired stopping event. This is only applicable to HKG CAN platforms using openpilot longitudinal control.
-
-
-
- Subaru
-
-
-
- Manual Parking Brake: Stop and Go (Beta)
-
-
-
- Experimental feature to enable stop and go for Subaru Global models with manual handbrake. Models with electric parking brake should keep this disabled. Thanks to martinl for this implementation!
-
-
-
- Toyota/Lexus
-
-
-
- Enable Stock Toyota Longitudinal Control
-
-
-
- sunnypilot will <b>not</b> take over control of gas and brakes. Stock Toyota longitudinal control will be used.
-
-
-
- Allow M.A.D.S. toggling w/ LKAS Button (Beta)
-
-
-
- Allows M.A.D.S. engagement/disengagement with "LKAS" button from the steering wheel.
-
-
-
- Note: Enabling this toggle may have unexpected behavior with steering control. It is the driver's responsibility to observe their environment and make decisions accordingly.
-
-
-
- Toyota TSS2 Longitudinal: Custom Tuning
-
-
-
- Smoother longitudinal performance for Toyota/Lexus TSS2/LSS2 cars. Big thanks to dragonpilot-community for this implementation.
-
-
-
- Enable Toyota Stop and Go Hack
-
-
-
- sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. This feature is only applicable to certain models. Use at your own risk.
-
-
-
- Volkswagen
-
-
-
- Enable CC Only support
-
-
-
- sunnypilot supports Volkswagen MQB CC only platforms with this toggle enabled. Only enable this toggle if your car does not have ACC from the factory.
-
-
-
SettingsWindow
@@ -1341,7 +680,7 @@ This may take up to a minute.
Device
- 設備
+ 裝置
Network
@@ -1355,38 +694,6 @@ This may take up to a minute.
Software
軟體
-
- sunnylink
-
-
-
- sunnypilot
-
-
-
- OSM
-
-
-
- Monitoring
-
-
-
- Visuals
-
-
-
- Display
-
-
-
- Trips
-
-
-
- Vehicle
-
-
Setup
@@ -1448,7 +755,7 @@ This may take up to a minute.
Ensure the entered URL is valid, and the device’s internet connection is good.
- 請確定您輸入的是有效的安裝網址,並且確定設備的網路連線狀態良好。
+ 請確定您輸入的是有效的安裝網址,並且確定裝置的網路連線狀態良好。
Reboot device
@@ -1464,7 +771,7 @@ This may take up to a minute.
Something went wrong. Reboot the device.
- 發生了一些錯誤。請重新啟動您的設備。
+ 發生了一些錯誤。請重新啟動您的裝置。
Select a language
@@ -1491,11 +798,11 @@ This may take up to a minute.
Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.
- 將您的設備與 comma connect (connect.comma.ai) 配對並領取您的 comma 高級會員優惠。
+ 將您的裝置與 comma connect (connect.comma.ai) 配對並領取您的 comma 高級會員優惠。
Pair device
- 配對設備
+ 配對裝置
@@ -1580,65 +887,6 @@ This may take up to a minute.
5G
5G
-
- SUNNYLINK
-
-
-
- DISABLED
-
-
-
-
- SlcSettings
-
- Auto
-
-
-
- User Confirm
-
-
-
- Engage Mode
-
-
-
- Default
-
-
-
- Fixed
-
-
-
- Percentage
-
-
-
- Limit Offset
-
-
-
- Set speed limit slightly higher than actual speed limit for a more natural drive.
-
-
-
- This platform defaults to <b>Auto</b> mode. <b>User Confirm</b> mode is not supported on this platform.
-
-
-
- Select the desired mode to set the cruising speed to the speed limit:
-
-
-
- Auto: Automatic speed adjustment on motorways based on speed limit data.
-
-
-
- User Confirm: Inform the driver to change set speed of Adaptive Cruise Control to help the driver stay within the speed limit.
-
-
SoftwarePanel
@@ -1714,237 +962,6 @@ This may take up to a minute.
never
從未更新
-
- Driving Model
-
-
-
-
- SoftwarePanelSP
-
- Driving Model
-
-
-
- SELECT
- 選取
-
-
- Downloading Driving model
-
-
-
- (CACHED)
-
-
-
- Driving model
-
-
-
- downloaded
-
-
-
- Downloading Navigation model
-
-
-
- Navigation model
-
-
-
- Downloading Metadata model
-
-
-
- Metadata model
-
-
-
- Downloads have failed, please try swapping the model!
-
-
-
- Failed:
-
-
-
- Fetching models...
-
-
-
- Select a Driving Model
-
-
-
- Download has started in the background.
-
-
-
- We STRONGLY suggest you to reset calibration. Would you like to do that now?
-
-
-
- Reset Calibration
- 重設校準
-
-
- Warning: You are on a metered connection!
-
-
-
- Continue
- 繼續
-
-
- on Metered
-
-
-
- PENDING
-
-
-
-
- SpeedLimitPolicySettings
-
- Speed Limit Source Policy
-
-
-
- Nav
-
-
-
- Only
-
-
-
- Map
-
-
-
- Car
-
-
-
- First
-
-
-
- Select the precedence order of sources. Utilized by Speed Limit Control and Speed Limit Warning
-
-
-
- Nav Only: Data from Mapbox active navigation only.
-
-
-
- Map Only: Data from OpenStreetMap only.
-
-
-
- Car Only: Data from the car's built-in sources (if available).
-
-
-
- Nav First: Nav -> Map -> Car
-
-
-
- Map First: Map -> Nav -> Car
-
-
-
- Car First: Car -> Nav -> Map
-
-
-
-
- SpeedLimitValueOffset
-
- km/h
- km/h
-
-
- mph
- mph
-
-
-
- SpeedLimitWarningSettings
-
- Off
-
-
-
- Display
-
-
-
- Chime
-
-
-
- Speed Limit Warning
-
-
-
- Warning with speed limit flash
-
-
-
- When Speed Limit Warning is enabled, the speed limit sign will alert the driver when the cruising speed is faster than then speed limit plus the offset.
-
-
-
- Default
-
-
-
- Fixed
-
-
-
- Percentage
-
-
-
- Warning Offset
-
-
-
- Select the desired offset to warn the driver when the vehicle is driving faster than the speed limit.
-
-
-
- Off: When the cruising speed is faster than the speed limit plus the offset, there will be no warning.
-
-
-
- Display: The speed on the speed limit sign turns red to alert the driver when the cruising speed is faster than the speed limit plus the offset.
-
-
-
- Chime: The speed on the speed limit sign turns red and chimes to alert the driver when the cruising speed is faster than the speed limit plus the offset.
-
-
-
-
- SpeedLimitWarningValueOffset
-
- N/A
- 無法使用
-
-
- km/h
- km/h
-
-
- mph
- mph
-
SshControl
@@ -1992,419 +1009,6 @@ This may take up to a minute.
啟用 SSH 服務
-
- SunnylinkPanel
-
- Enable sunnylink
-
-
-
- Device ID
-
-
-
- N/A
- 無法使用
-
-
- This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that.
-
-
-
- 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀
-
-
-
- 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉.
-
-
-
- A reboot is required to
-
-
-
- start
-
-
-
- stop
-
-
-
- all connections and processes from sunnylink.
-
-
-
- If that's not a problem for you, you can ignore this.
-
-
-
- Reboot Now!
-
-
-
- Sponsor Status
-
-
-
- SPONSOR
-
-
-
- Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.
-
-
-
- Pair GitHub Account
-
-
-
- PAIR
-
-
-
- Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.
-
-
-
- Manage Settings
-
-
-
- Backup Settings
-
-
-
- Are you sure you want to backup sunnypilot settings?
-
-
-
- Back Up
-
-
-
- Restore Settings
-
-
-
- Are you sure you want to restore the last backed up sunnypilot settings?
-
-
-
- Restore
-
-
-
- THANKS
-
-
-
- Not Sponsor
-
-
-
- Paired
-
-
-
- Not Paired
-
-
-
- Backing up...
-
-
-
- Restoring...
-
-
-
-
- SunnylinkSponsorPopup
-
- Scan the QR code to login to your GitHub account
-
-
-
- Follow the prompts to complete the pairing process
-
-
-
- Re-enter the "sunnylink" panel to verify sponsorship status
-
-
-
- If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot
-
-
-
- Scan the QR code to visit sunnyhaibin's GitHub Sponsors page
-
-
-
- Choose your sponsorship tier and confirm your support
-
-
-
- Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status
-
-
-
- Pair your GitHub account
-
-
-
- Early Access: Become a sunnypilot Sponsor
-
-
-
-
- SunnypilotPanel
-
- Enable M.A.D.S.
-
-
-
- Enable the beloved M.A.D.S. feature. Disable toggle to revert back to stock openpilot engagement/disengagement.
-
-
-
- Laneless for Curves in "Auto" Mode
-
-
-
- While in Auto Lane, switch to Laneless for current/future curves.
-
-
-
- Speed Limit Control (SLC)
-
-
-
- When you engage ACC, you will be prompted to set the cruising speed to the speed limit of the road adjusted by the Offset and Source Policy specified, or the current driving speed. The maximum cruising speed will always be the MAX set speed.
-
-
-
- Enable Vision-based Turn Speed Control (V-TSC)
-
-
-
- Use vision path predictions to estimate the appropriate speed to drive through turns ahead.
-
-
-
- Enable Map Data Turn Speed Control (M-TSC) (Beta)
-
-
-
- Use curvature information from map data to define speed limits to take turns ahead.
-
-
-
- ACC +/-: Long Press Reverse
-
-
-
- Change the ACC +/- buttons behavior with cruise speed change in sunnypilot.
-
-
-
- Disabled (Stock): Short=1, Long = 5 (imperial) / 10 (metric)
-
-
-
- Enabled: Short = 5 (imperial) / 10 (metric), Long=1
-
-
-
- Custom Offsets
-
-
-
- Neural Network Lateral Control (NNLC)
-
-
-
- Enforce Torque Lateral Control
-
-
-
- Enable this to enforce sunnypilot to steer with Torque lateral control.
-
-
-
- Enable Self-Tune
-
-
-
- Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default.
-
-
-
- Less Restrict Settings for Self-Tune (Beta)
-
-
-
- Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values.
-
-
-
- Enable Custom Tuning
-
-
-
- Enables custom tuning for Torque lateral control. Modifying FRICTION and LAT_ACCEL_FACTOR below will override the offline values indicated in the YAML files within "selfdrive/torque_data". The values will also be used live when "Override Self-Tune" toggle is enabled.
-
-
-
- Manual Real-Time Tuning
-
-
-
- Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune. Enabling this toggle overrides Self-Tune values.
-
-
-
- Quiet Drive 🤫
-
-
-
- sunnypilot will display alerts but only play the most important warning sounds. This feature can be toggled while the car is on.
-
-
-
- Green Traffic Light Chime (Beta)
-
-
-
- A chime will play when the traffic light you are waiting for turns green and you have no vehicle in front of you. If you are waiting behind another vehicle, the chime will play once the vehicle advances unless ACC is engaged.
-
-
-
- Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly.
-
-
-
- Lead Vehicle Departure Alert
-
-
-
- Enable this will notify when the leading vehicle drives away.
-
-
-
- Customize M.A.D.S.
-
-
-
- Customize Lane Change
-
-
-
- Customize Offsets
-
-
-
- Customize Speed Limit Control
-
-
-
- Customize Warning
-
-
-
- Customize Source
-
-
-
- Laneful
-
-
-
- Laneless
-
-
-
- Auto
-
-
-
- Dynamic Lane Profile
-
-
-
- Speed Limit Assist
-
-
-
- Real-time and Offline
-
-
-
- Offline Only
-
-
-
- Dynamic Lane Profile is not available with the current Driving Model
-
-
-
- Custom Offsets is not available with the current Driving Model
-
-
-
- NNLC is currently not available on this platform.
-
-
-
- Match: "Exact" is ideal, but "Fuzzy" is fine too. Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server if there are any issues:
-
-
-
- Start the car to check car compatibility
-
-
-
- NNLC Not Loaded
-
-
-
- NNLC Loaded
-
-
-
- Fuzzy
-
-
-
- Exact
-
-
-
- Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server and donate logs to get NNLC loaded for your car:
-
-
-
- Match
-
-
-
- Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy.
-
-
-
- Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server with feedback, or to provide log data for your car if your car is currently unsupported:
-
-
-
- Add custom offsets to Camera and Path in sunnypilot.
-
-
-
- Default is Laneless. In Auto mode, sunnnypilot dynamically chooses between Laneline or Laneless model based on lane recognition confidence level on road and certain conditions.
-
-
-
TermsPage
@@ -2428,11 +1032,11 @@ This may take up to a minute.
TogglesPanel
Enable openpilot
- 啟用 openpilot
+ 啟用 openpilot
Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off.
- 使用 openpilot 的主動式巡航和車道保持功能,開啟後您需要持續集中注意力,設定變更在重新啟動車輛後生效。
+ 使用 openpilot 的主動式巡航和車道保持功能,開啟後您需要持續集中注意力,設定變更在重新啟動車輛後生效。
Enable Lane Departure Warnings
@@ -2524,7 +1128,7 @@ This may take up to a minute.
Standard
- 標準
+ 標準
Relaxed
@@ -2546,9 +1150,13 @@ This may take up to a minute.
End-to-End Longitudinal Control
端到端縱向控制
+
+ Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button.
+ 建議使用標準模式。在積極模式下,openpilot 會更接近前車並更積極地使用油門和剎車。在輕鬆模式下,openpilot 會與前車保持較遠距離。對於支援的汽車,您可以使用方向盤上的距離按鈕來切換這些駕駛風格。
+
The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner.
-
+ 在低速時,駕駛可視化將切換至道路朝向的廣角攝影機,以更好地顯示某些彎道。在右上角還會顯示「實驗模式」的標誌。
Always-On Driver Monitoring
@@ -2558,89 +1166,6 @@ This may take up to a minute.
Enable driver monitoring even when openpilot is not engaged.
即使在openpilot未激活時也啟用駕駛監控。
-
- Enable sunnypilot
-
-
-
- Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off.
-
-
-
- Custom Stock Longitudinal Control
-
-
-
- When enabled, sunnypilot will attempt to control stock longitudinal control with ACC button presses.
-This feature must be used along with SLC, and/or V-TSC, and/or M-TSC.
-
-
-
- Enable Dynamic Experimental Control
-
-
-
- Enable toggle to allow the model to determine when to use openpilot ACC or openpilot End to End Longitudinal.
-
-
-
- Disable Onroad Uploads
-
-
-
- Disable uploads completely when onroad. Necessary to avoid high data usage when connected to Wi-Fi hotspot. Turn on this feature if you are looking to utilize map-based features, such as Speed Limit Control (SLC) and Map-based Turn Speed Control (MTSC).
-
-
-
- Maniac
-
-
-
- Stock
-
-
-
- Stock is recommended. In aggressive/maniac mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button.
-
-
-
-
- TorqueFriction
-
- FRICTION
-
-
-
- Adjust Friction for the Torque Lateral Controller. <b>Live</b>: Override self-tune values; <b>Offline</b>: Override self-tune offline values at car restart.
-
-
-
- Real-time and Offline
-
-
-
- Offline Only
-
-
-
-
- TorqueMaxLatAccel
-
- LAT_ACCEL_FACTOR
-
-
-
- Adjust Max Lateral Acceleration for the Torque Lateral Controller. <b>Live</b>: Override self-tune values; <b>Offline</b>: Override self-tune offline values at car restart.
-
-
-
- Real-time and Offline
-
-
-
- Offline Only
-
-
Updater
@@ -2650,7 +1175,7 @@ This feature must be used along with SLC, and/or V-TSC, and/or M-TSC.
An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB.
- 設備的操作系統需要更新。請將您的設備連接到 Wi-Fi 以獲得最快的更新體驗。下載大小約為 1GB。
+ 需要進行作業系統更新。建議將您的裝置連接上 Wi-Fi 獲得更快的更新下載。下載大小約為 1GB。
Connect to Wi-Fi
@@ -2677,165 +1202,6 @@ This feature must be used along with SLC, and/or V-TSC, and/or M-TSC.
更新失敗
-
- VehiclePanel
-
- Updating this setting takes effect when the car is powered off.
-
-
-
- Select your car
-
-
-
-
- VisualsPanel
-
- Display Braking Status
-
-
-
- Enable this will turn the current speed value to red while the brake is used.
-
-
-
- Display Stand Still Timer
-
-
-
- Enable this will display time spent at a stop (i.e., at a stop lights, stop signs, traffic congestions).
-
-
-
- Display DM Camera in Reverse Gear
-
-
-
- Show Driver Monitoring camera while the car is in reverse gear.
-
-
-
- OSM: Show debug UI elements
-
-
-
- OSM: Show UI elements that aid debugging.
-
-
-
- Display Feature Status
-
-
-
- Display the statuses of certain features on the driving screen.
-
-
-
- Enable Onroad Settings
-
-
-
- Display the Onroad Settings button on the driving screen to adjust feature options on the driving screen, without navigating into the settings menu.
-
-
-
- Speedometer: Display True Speed
-
-
-
- Display the true vehicle current speed from wheel speed sensors.
-
-
-
- Speedometer: Hide from Onroad Screen
-
-
-
- Display End-to-end Longitudinal Status (Beta)
-
-
-
- Enable this will display an icon that appears when the End-to-end model decides to start or stop.
-
-
-
- Navigation: Display in Full Screen
-
-
-
- Enable this will display the built-in navigation in full screen.<br>To switch back to driving view, <font color='yellow'>tap on the border edge</font>.
-
-
-
- Map: Display 3D Buildings
-
-
-
- Parse and display 3D buildings on map. Thanks to jakethesnake420 for this implementation.
-
-
-
- Off
-
-
-
- 5 Metrics
-
-
-
- 10 Metrics
-
-
-
- Developer UI
-
-
-
- Display real-time parameters and metrics from various sources.
-
-
-
- Distance
-
-
-
- Speed
-
-
-
- Distance
-Speed
-
-
-
- Display Metrics Below Chevron
-
-
-
- Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control).
-
-
-
- RAM
-
-
-
- CPU
-
-
-
- GPU
-
-
-
- Max
-
-
-
- Display Temperature on Sidebar
-
-
-
WiFiPromptWidget
diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc
index 9afd22f13a..c70b7594c9 100644
--- a/selfdrive/ui/ui.cc
+++ b/selfdrive/ui/ui.cc
@@ -57,26 +57,23 @@ void update_leads(UIState *s, const cereal::RadarState::Reader &radar_state, con
void update_line_data(const UIState *s, const cereal::XYZTData::Reader &line,
float y_off, float z_off, QPolygonF *pvd, int max_idx, bool allow_invert=true) {
const auto line_x = line.getX(), line_y = line.getY(), line_z = line.getZ();
- QPolygonF left_points, right_points;
- left_points.reserve(max_idx + 1);
- right_points.reserve(max_idx + 1);
-
+ QPointF left, right;
+ pvd->clear();
for (int i = 0; i <= max_idx; i++) {
// highly negative x positions are drawn above the frame and cause flickering, clip to zy plane of camera
if (line_x[i] < 0) continue;
- QPointF left, right;
+
bool l = calib_frame_to_full_frame(s, line_x[i], line_y[i] - y_off, line_z[i] + z_off, &left);
bool r = calib_frame_to_full_frame(s, line_x[i], line_y[i] + y_off, line_z[i] + z_off, &right);
if (l && r) {
// For wider lines the drawn polygon will "invert" when going over a hill and cause artifacts
- if (!allow_invert && left_points.size() && left.y() > left_points.back().y()) {
+ if (!allow_invert && pvd->size() && left.y() > pvd->back().y()) {
continue;
}
- left_points.push_back(left);
- right_points.push_front(right);
+ pvd->push_back(left);
+ pvd->push_front(right);
}
}
- *pvd = left_points + right_points;
}
void update_model(UIState *s,
@@ -128,25 +125,27 @@ void update_dmonitoring(UIState *s, const cereal::DriverStateV2::Reader &drivers
scene.driver_pose_coss[i] = cosf(scene.driver_pose_vals[i]*(1.0-dm_fade_state));
}
+ auto [sin_y, sin_x, sin_z] = scene.driver_pose_sins;
+ auto [cos_y, cos_x, cos_z] = scene.driver_pose_coss;
+
const mat3 r_xyz = (mat3){{
- scene.driver_pose_coss[1]*scene.driver_pose_coss[2],
- scene.driver_pose_coss[1]*scene.driver_pose_sins[2],
- -scene.driver_pose_sins[1],
+ cos_x * cos_z,
+ cos_x * sin_z,
+ -sin_x,
- -scene.driver_pose_sins[0]*scene.driver_pose_sins[1]*scene.driver_pose_coss[2] - scene.driver_pose_coss[0]*scene.driver_pose_sins[2],
- -scene.driver_pose_sins[0]*scene.driver_pose_sins[1]*scene.driver_pose_sins[2] + scene.driver_pose_coss[0]*scene.driver_pose_coss[2],
- -scene.driver_pose_sins[0]*scene.driver_pose_coss[1],
+ -sin_y * sin_x * cos_z - cos_y * sin_z,
+ -sin_y * sin_x * sin_z + cos_y * cos_z,
+ -sin_y * cos_x,
- scene.driver_pose_coss[0]*scene.driver_pose_sins[1]*scene.driver_pose_coss[2] - scene.driver_pose_sins[0]*scene.driver_pose_sins[2],
- scene.driver_pose_coss[0]*scene.driver_pose_sins[1]*scene.driver_pose_sins[2] + scene.driver_pose_sins[0]*scene.driver_pose_coss[2],
- scene.driver_pose_coss[0]*scene.driver_pose_coss[1],
+ cos_y * sin_x * cos_z - sin_y * sin_z,
+ cos_y * sin_x * sin_z + sin_y * cos_z,
+ cos_y * cos_x,
}};
// transform vertices
for (int kpi = 0; kpi < std::size(default_face_kpts_3d); kpi++) {
- vec3 kpt_this = default_face_kpts_3d[kpi];
- kpt_this = matvecmul3(r_xyz, kpt_this);
- scene.face_kpts_draw[kpi] = (vec3){{(float)kpt_this.v[0], (float)kpt_this.v[1], (float)(kpt_this.v[2] * (1.0-dm_fade_state) + 8 * dm_fade_state)}};
+ vec3 kpt_this = matvecmul3(r_xyz, default_face_kpts_3d[kpi]);
+ scene.face_kpts_draw[kpi] = (vec3){{kpt_this.v[0], kpt_this.v[1], (float)(kpt_this.v[2] * (1.0-dm_fade_state) + 8 * dm_fade_state)}};
}
}
@@ -205,6 +204,8 @@ static void update_state(UIState *s) {
auto cam_state = sm["wideRoadCameraState"].getWideRoadCameraState();
float scale = (cam_state.getSensor() == cereal::FrameData::ImageSensor::AR0231) ? 6.0f : 1.0f;
scene.light_sensor = std::max(100.0f - scale * cam_state.getExposureValPercent(), 0.0f);
+ } else if (!sm.allAliveAndValid({"wideRoadCameraState"})) {
+ scene.light_sensor = -1;
}
scene.started = sm["deviceState"].getDeviceState().getStarted() && scene.ignition;
@@ -248,7 +249,7 @@ UIState::UIState(QObject *parent) : QObject(parent) {
sm = std::make_unique>({
"modelV2", "controlsState", "liveCalibration", "radarState", "deviceState",
"pandaStates", "carParams", "driverMonitoringState", "carState", "liveLocationKalman", "driverStateV2",
- "wideRoadCameraState", "managerState", "navInstruction", "navRoute", "uiPlan",
+ "wideRoadCameraState", "managerState", "navInstruction", "navRoute", "uiPlan", "clocks",
});
Params params;
@@ -320,7 +321,7 @@ void Device::resetInteractiveTimeout(int timeout) {
void Device::updateBrightness(const UIState &s) {
float clipped_brightness = offroad_brightness;
- if (s.scene.started) {
+ if (s.scene.started && s.scene.light_sensor > 0) {
clipped_brightness = s.scene.light_sensor;
// CIE 1931 - https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htm
diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h
index f5028a280f..7238159dda 100644
--- a/selfdrive/ui/ui.h
+++ b/selfdrive/ui/ui.h
@@ -97,7 +97,7 @@ typedef struct UIScene {
cereal::LongitudinalPersonality personality;
- float light_sensor;
+ float light_sensor = -1;
bool started, ignition, is_metric, map_on_left, longitudinal_control;
bool world_objects_visible = false;
uint64_t started_frame;
diff --git a/selfdrive/athena/__init__.py b/system/athena/__init__.py
similarity index 100%
rename from selfdrive/athena/__init__.py
rename to system/athena/__init__.py
diff --git a/selfdrive/athena/athenad.py b/system/athena/athenad.py
similarity index 100%
rename from selfdrive/athena/athenad.py
rename to system/athena/athenad.py
diff --git a/selfdrive/athena/manage_athenad.py b/system/athena/manage_athenad.py
similarity index 88%
rename from selfdrive/athena/manage_athenad.py
rename to system/athena/manage_athenad.py
index 2ec92cc3e0..f5ab817206 100755
--- a/selfdrive/athena/manage_athenad.py
+++ b/system/athena/manage_athenad.py
@@ -4,7 +4,7 @@ import time
from multiprocessing import Process
from openpilot.common.params import Params
-from openpilot.selfdrive.manager.process import launcher
+from openpilot.system.manager.process import launcher
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import HARDWARE
from openpilot.system.version import get_build_metadata
@@ -28,7 +28,7 @@ def main():
try:
while 1:
cloudlog.info("starting athena daemon")
- proc = Process(name='athenad', target=launcher, args=('selfdrive.athena.athenad', 'athenad'))
+ proc = Process(name='athenad', target=launcher, args=('system.athena.athenad', 'athenad'))
proc.start()
proc.join()
cloudlog.event("athenad exited", exitcode=proc.exitcode)
diff --git a/selfdrive/athena/registration.py b/system/athena/registration.py
similarity index 100%
rename from selfdrive/athena/registration.py
rename to system/athena/registration.py
diff --git a/selfdrive/athena/tests/__init__.py b/system/athena/tests/__init__.py
similarity index 100%
rename from selfdrive/athena/tests/__init__.py
rename to system/athena/tests/__init__.py
diff --git a/selfdrive/athena/tests/helpers.py b/system/athena/tests/helpers.py
similarity index 95%
rename from selfdrive/athena/tests/helpers.py
rename to system/athena/tests/helpers.py
index 322e9d81dd..a0a9cccdc1 100644
--- a/selfdrive/athena/tests/helpers.py
+++ b/system/athena/tests/helpers.py
@@ -9,7 +9,7 @@ class MockResponse:
self.status_code = status_code
-class EchoSocket():
+class EchoSocket:
def __init__(self, port):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.bind(('127.0.0.1', port))
@@ -34,7 +34,7 @@ class EchoSocket():
self.socket.close()
-class MockApi():
+class MockApi:
def __init__(self, dongle_id):
pass
@@ -42,7 +42,7 @@ class MockApi():
return "fake-token"
-class MockWebsocket():
+class MockWebsocket:
sock = socket.socket()
def __init__(self, recv_queue, send_queue):
diff --git a/selfdrive/athena/tests/test_athenad.py b/system/athena/tests/test_athenad.py
similarity index 69%
rename from selfdrive/athena/tests/test_athenad.py
rename to system/athena/tests/test_athenad.py
index 4850ab9a3f..895cd8eb56 100755
--- a/selfdrive/athena/tests/test_athenad.py
+++ b/system/athena/tests/test_athenad.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
-from functools import partial, wraps
+import pytest
+from functools import wraps
import json
import multiprocessing
import os
@@ -8,12 +9,9 @@ import shutil
import time
import threading
import queue
-import unittest
from dataclasses import asdict, replace
from datetime import datetime, timedelta
-from parameterized import parameterized
-from unittest import mock
from websocket import ABNF
from websocket._exceptions import WebSocketConnectionClosedException
@@ -21,10 +19,10 @@ from cereal import messaging
from openpilot.common.params import Params
from openpilot.common.timeout import Timeout
-from openpilot.selfdrive.athena import athenad
-from openpilot.selfdrive.athena.athenad import MAX_RETRY_COUNT, dispatcher
-from openpilot.selfdrive.athena.tests.helpers import HTTPRequestHandler, MockWebsocket, MockApi, EchoSocket
-from openpilot.selfdrive.test.helpers import with_http_server
+from openpilot.system.athena import athenad
+from openpilot.system.athena.athenad import MAX_RETRY_COUNT, dispatcher
+from openpilot.system.athena.tests.helpers import HTTPRequestHandler, MockWebsocket, MockApi, EchoSocket
+from openpilot.selfdrive.test.helpers import http_server_context
from openpilot.system.hardware.hw import Paths
@@ -37,10 +35,6 @@ def seed_athena_server(host, port):
except requests.exceptions.ConnectionError:
time.sleep(0.1)
-
-with_mock_athena = partial(with_http_server, handler=HTTPRequestHandler, setup=seed_athena_server)
-
-
def with_upload_handler(func):
@wraps(func)
def wrapper(*args, **kwargs):
@@ -54,15 +48,23 @@ def with_upload_handler(func):
thread.join()
return wrapper
+@pytest.fixture
+def mock_create_connection(mocker):
+ return mocker.patch('openpilot.system.athena.athenad.create_connection')
-class TestAthenadMethods(unittest.TestCase):
+@pytest.fixture
+def host():
+ with http_server_context(handler=HTTPRequestHandler, setup=seed_athena_server) as (host, port):
+ yield f"http://{host}:{port}"
+
+class TestAthenadMethods:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
cls.SOCKET_PORT = 45454
athenad.Api = MockApi
athenad.LOCAL_PORT_WHITELIST = {cls.SOCKET_PORT}
- def setUp(self):
+ def setup_method(self):
self.default_params = {
"DongleId": "0000000000000000",
"GithubSshKeys": b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC307aE+nuHzTAgaJhzSf5v7ZZQW9gaperjhCmyPyl4PzY7T1mDGenTlVTN7yoVFZ9UfO9oMQqo0n1OwDIiqbIFxqnhrHU0cYfj88rI85m5BEKlNu5RdaVTj1tcbaPpQc5kZEolaI1nDDjzV0lwS7jo5VYDHseiJHlik3HH1SgtdtsuamGR2T80q1SyW+5rHoMOJG73IH2553NnWuikKiuikGHUYBd00K1ilVAK2xSiMWJp55tQfZ0ecr9QjEsJ+J/efL4HqGNXhffxvypCXvbUYAFSddOwXUPo5BTKevpxMtH+2YrkpSjocWA04VnTYFiPG6U4ItKmbLOTFZtPzoez private", # noqa: E501
@@ -109,8 +111,8 @@ class TestAthenadMethods(unittest.TestCase):
def test_echo(self):
assert dispatcher["echo"]("bob") == "bob"
- def test_getMessage(self):
- with self.assertRaises(TimeoutError) as _:
+ def test_get_message(self):
+ with pytest.raises(TimeoutError) as _:
dispatcher["getMessage"]("controlsState")
end_event = multiprocessing.Event()
@@ -133,7 +135,7 @@ class TestAthenadMethods(unittest.TestCase):
end_event.set()
p.join()
- def test_listDataDirectory(self):
+ def test_list_data_directory(self):
route = '2021-03-29--13-32-47'
segments = [0, 1, 2, 3, 11]
@@ -143,69 +145,66 @@ class TestAthenadMethods(unittest.TestCase):
self._create_file(file)
resp = dispatcher["listDataDirectory"]()
- self.assertTrue(resp, 'list empty!')
- self.assertCountEqual(resp, files)
+ assert resp, 'list empty!'
+ assert len(resp) == len(files)
resp = dispatcher["listDataDirectory"](f'{route}--123')
- self.assertCountEqual(resp, [])
+ assert len(resp) == 0
prefix = f'{route}'
- expected = filter(lambda f: f.startswith(prefix), files)
+ expected = list(filter(lambda f: f.startswith(prefix), files))
resp = dispatcher["listDataDirectory"](prefix)
- self.assertTrue(resp, 'list empty!')
- self.assertCountEqual(resp, expected)
+ assert resp, 'list empty!'
+ assert len(resp) == len(expected)
prefix = f'{route}--1'
- expected = filter(lambda f: f.startswith(prefix), files)
+ expected = list(filter(lambda f: f.startswith(prefix), files))
resp = dispatcher["listDataDirectory"](prefix)
- self.assertTrue(resp, 'list empty!')
- self.assertCountEqual(resp, expected)
+ assert resp, 'list empty!'
+ assert len(resp) == len(expected)
prefix = f'{route}--1/'
- expected = filter(lambda f: f.startswith(prefix), files)
+ expected = list(filter(lambda f: f.startswith(prefix), files))
resp = dispatcher["listDataDirectory"](prefix)
- self.assertTrue(resp, 'list empty!')
- self.assertCountEqual(resp, expected)
+ assert resp, 'list empty!'
+ assert len(resp) == len(expected)
prefix = f'{route}--1/q'
- expected = filter(lambda f: f.startswith(prefix), files)
+ expected = list(filter(lambda f: f.startswith(prefix), files))
resp = dispatcher["listDataDirectory"](prefix)
- self.assertTrue(resp, 'list empty!')
- self.assertCountEqual(resp, expected)
+ assert resp, 'list empty!'
+ assert len(resp) == len(expected)
def test_strip_bz2_extension(self):
fn = self._create_file('qlog.bz2')
if fn.endswith('.bz2'):
- self.assertEqual(athenad.strip_bz2_extension(fn), fn[:-4])
+ assert athenad.strip_bz2_extension(fn) == fn[:-4]
- @parameterized.expand([(True,), (False,)])
- @with_mock_athena
- def test_do_upload(self, compress, host):
+ @pytest.mark.parametrize("compress", [True, False])
+ def test_do_upload(self, host, compress):
# random bytes to ensure rather large object post-compression
fn = self._create_file('qlog', data=os.urandom(10000 * 1024))
upload_fn = fn + ('.bz2' if compress else '')
item = athenad.UploadItem(path=upload_fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='')
- with self.assertRaises(requests.exceptions.ConnectionError):
+ with pytest.raises(requests.exceptions.ConnectionError):
athenad._do_upload(item)
item = athenad.UploadItem(path=upload_fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='')
resp = athenad._do_upload(item)
- self.assertEqual(resp.status_code, 201)
+ assert resp.status_code == 201
- @with_mock_athena
- def test_uploadFileToUrl(self, host):
+ def test_upload_file_to_url(self, host):
fn = self._create_file('qlog.bz2')
resp = dispatcher["uploadFileToUrl"]("qlog.bz2", f"{host}/qlog.bz2", {})
- self.assertEqual(resp['enqueued'], 1)
- self.assertNotIn('failed', resp)
- self.assertLessEqual({"path": fn, "url": f"{host}/qlog.bz2", "headers": {}}.items(), resp['items'][0].items())
- self.assertIsNotNone(resp['items'][0].get('id'))
- self.assertEqual(athenad.upload_queue.qsize(), 1)
+ assert resp['enqueued'] == 1
+ assert 'failed' not in resp
+ assert {"path": fn, "url": f"{host}/qlog.bz2", "headers": {}}.items() <= resp['items'][0].items()
+ assert resp['items'][0].get('id') is not None
+ assert athenad.upload_queue.qsize() == 1
- @with_mock_athena
- def test_uploadFileToUrl_duplicate(self, host):
+ def test_upload_file_to_url_duplicate(self, host):
self._create_file('qlog.bz2')
url1 = f"{host}/qlog.bz2?sig=sig1"
@@ -214,14 +213,12 @@ class TestAthenadMethods(unittest.TestCase):
# Upload same file again, but with different signature
url2 = f"{host}/qlog.bz2?sig=sig2"
resp = dispatcher["uploadFileToUrl"]("qlog.bz2", url2, {})
- self.assertEqual(resp, {'enqueued': 0, 'items': []})
+ assert resp == {'enqueued': 0, 'items': []}
- @with_mock_athena
- def test_uploadFileToUrl_does_not_exist(self, host):
+ def test_upload_file_to_url_does_not_exist(self, host):
not_exists_resp = dispatcher["uploadFileToUrl"]("does_not_exist.bz2", "http://localhost:1238", {})
- self.assertEqual(not_exists_resp, {'enqueued': 0, 'items': [], 'failed': ['does_not_exist.bz2']})
+ assert not_exists_resp == {'enqueued': 0, 'items': [], 'failed': ['does_not_exist.bz2']}
- @with_mock_athena
@with_upload_handler
def test_upload_handler(self, host):
fn = self._create_file('qlog.bz2')
@@ -233,13 +230,12 @@ class TestAthenadMethods(unittest.TestCase):
# TODO: verify that upload actually succeeded
# TODO: also check that end_event and metered network raises AbortTransferException
- self.assertEqual(athenad.upload_queue.qsize(), 0)
+ assert athenad.upload_queue.qsize() == 0
- @parameterized.expand([(500, True), (412, False)])
- @with_mock_athena
- @mock.patch('requests.put')
+ @pytest.mark.parametrize("status,retry", [(500,True), (412,False)])
@with_upload_handler
- def test_upload_handler_retry(self, status, retry, mock_put, host):
+ def test_upload_handler_retry(self, mocker, host, status, retry):
+ mock_put = mocker.patch('requests.put')
mock_put.return_value.status_code = status
fn = self._create_file('qlog.bz2')
item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True)
@@ -248,10 +244,10 @@ class TestAthenadMethods(unittest.TestCase):
self._wait_for_upload()
time.sleep(0.1)
- self.assertEqual(athenad.upload_queue.qsize(), 1 if retry else 0)
+ assert athenad.upload_queue.qsize() == (1 if retry else 0)
if retry:
- self.assertEqual(athenad.upload_queue.get().retry_count, 1)
+ assert athenad.upload_queue.get().retry_count == 1
@with_upload_handler
def test_upload_handler_timeout(self):
@@ -265,33 +261,33 @@ class TestAthenadMethods(unittest.TestCase):
time.sleep(0.1)
# Check that upload with retry count exceeded is not put back
- self.assertEqual(athenad.upload_queue.qsize(), 0)
+ assert athenad.upload_queue.qsize() == 0
athenad.upload_queue.put_nowait(item)
self._wait_for_upload()
time.sleep(0.1)
# Check that upload item was put back in the queue with incremented retry count
- self.assertEqual(athenad.upload_queue.qsize(), 1)
- self.assertEqual(athenad.upload_queue.get().retry_count, 1)
+ assert athenad.upload_queue.qsize() == 1
+ assert athenad.upload_queue.get().retry_count == 1
@with_upload_handler
- def test_cancelUpload(self):
+ def test_cancel_upload(self):
item = athenad.UploadItem(path="qlog.bz2", url="http://localhost:44444/qlog.bz2", headers={},
created_at=int(time.time()*1000), id='id', allow_cellular=True)
athenad.upload_queue.put_nowait(item)
dispatcher["cancelUpload"](item.id)
- self.assertIn(item.id, athenad.cancelled_uploads)
+ assert item.id in athenad.cancelled_uploads
self._wait_for_upload()
time.sleep(0.1)
- self.assertEqual(athenad.upload_queue.qsize(), 0)
- self.assertEqual(len(athenad.cancelled_uploads), 0)
+ assert athenad.upload_queue.qsize() == 0
+ assert len(athenad.cancelled_uploads) == 0
@with_upload_handler
- def test_cancelExpiry(self):
+ def test_cancel_expiry(self):
t_future = datetime.now() - timedelta(days=40)
ts = int(t_future.strftime("%s")) * 1000
@@ -303,15 +299,14 @@ class TestAthenadMethods(unittest.TestCase):
self._wait_for_upload()
time.sleep(0.1)
- self.assertEqual(athenad.upload_queue.qsize(), 0)
+ assert athenad.upload_queue.qsize() == 0
- def test_listUploadQueueEmpty(self):
+ def test_list_upload_queue_empty(self):
items = dispatcher["listUploadQueue"]()
- self.assertEqual(len(items), 0)
+ assert len(items) == 0
- @with_http_server
@with_upload_handler
- def test_listUploadQueueCurrent(self, host: str):
+ def test_list_upload_queue_current(self, host: str):
fn = self._create_file('qlog.bz2')
item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True)
@@ -319,22 +314,22 @@ class TestAthenadMethods(unittest.TestCase):
self._wait_for_upload()
items = dispatcher["listUploadQueue"]()
- self.assertEqual(len(items), 1)
- self.assertTrue(items[0]['current'])
+ assert len(items) == 1
+ assert items[0]['current']
- def test_listUploadQueue(self):
+ def test_list_upload_queue(self):
item = athenad.UploadItem(path="qlog.bz2", url="http://localhost:44444/qlog.bz2", headers={},
created_at=int(time.time()*1000), id='id', allow_cellular=True)
athenad.upload_queue.put_nowait(item)
items = dispatcher["listUploadQueue"]()
- self.assertEqual(len(items), 1)
- self.assertDictEqual(items[0], asdict(item))
- self.assertFalse(items[0]['current'])
+ assert len(items) == 1
+ assert items[0] == asdict(item)
+ assert not items[0]['current']
athenad.cancelled_uploads.add(item.id)
items = dispatcher["listUploadQueue"]()
- self.assertEqual(len(items), 0)
+ assert len(items) == 0
def test_upload_queue_persistence(self):
item1 = athenad.UploadItem(path="_", url="_", headers={}, created_at=int(time.time()), id='id1')
@@ -353,11 +348,10 @@ class TestAthenadMethods(unittest.TestCase):
athenad.upload_queue.queue.clear()
athenad.UploadQueueCache.initialize(athenad.upload_queue)
- self.assertEqual(athenad.upload_queue.qsize(), 1)
- self.assertDictEqual(asdict(athenad.upload_queue.queue[-1]), asdict(item1))
+ assert athenad.upload_queue.qsize() == 1
+ assert asdict(athenad.upload_queue.queue[-1]) == asdict(item1)
- @mock.patch('openpilot.selfdrive.athena.athenad.create_connection')
- def test_startLocalProxy(self, mock_create_connection):
+ def test_start_local_proxy(self, mock_create_connection):
end_event = threading.Event()
ws_recv = queue.Queue()
@@ -380,21 +374,21 @@ class TestAthenadMethods(unittest.TestCase):
ws_recv.put_nowait(WebSocketConnectionClosedException())
socket_thread.join()
- def test_getSshAuthorizedKeys(self):
+ def test_get_ssh_authorized_keys(self):
keys = dispatcher["getSshAuthorizedKeys"]()
- self.assertEqual(keys, self.default_params["GithubSshKeys"].decode('utf-8'))
+ assert keys == self.default_params["GithubSshKeys"].decode('utf-8')
- def test_getGithubUsername(self):
+ def test_get_github_username(self):
keys = dispatcher["getGithubUsername"]()
- self.assertEqual(keys, self.default_params["GithubUsername"].decode('utf-8'))
+ assert keys == self.default_params["GithubUsername"].decode('utf-8')
- def test_getVersion(self):
+ def test_get_version(self):
resp = dispatcher["getVersion"]()
keys = ["version", "remote", "branch", "commit"]
- self.assertEqual(list(resp.keys()), keys)
+ assert list(resp.keys()) == keys
for k in keys:
- self.assertIsInstance(resp[k], str, f"{k} is not a string")
- self.assertTrue(len(resp[k]) > 0, f"{k} has no value")
+ assert isinstance(resp[k], str), f"{k} is not a string"
+ assert len(resp[k]) > 0, f"{k} has no value"
def test_jsonrpc_handler(self):
end_event = threading.Event()
@@ -405,15 +399,15 @@ class TestAthenadMethods(unittest.TestCase):
# with params
athenad.recv_queue.put_nowait(json.dumps({"method": "echo", "params": ["hello"], "jsonrpc": "2.0", "id": 0}))
resp = athenad.send_queue.get(timeout=3)
- self.assertDictEqual(json.loads(resp), {'result': 'hello', 'id': 0, 'jsonrpc': '2.0'})
+ assert json.loads(resp) == {'result': 'hello', 'id': 0, 'jsonrpc': '2.0'}
# without params
athenad.recv_queue.put_nowait(json.dumps({"method": "getNetworkType", "jsonrpc": "2.0", "id": 0}))
resp = athenad.send_queue.get(timeout=3)
- self.assertDictEqual(json.loads(resp), {'result': 1, 'id': 0, 'jsonrpc': '2.0'})
+ assert json.loads(resp) == {'result': 1, 'id': 0, 'jsonrpc': '2.0'}
# log forwarding
athenad.recv_queue.put_nowait(json.dumps({'result': {'success': 1}, 'id': 0, 'jsonrpc': '2.0'}))
resp = athenad.log_recv_queue.get(timeout=3)
- self.assertDictEqual(json.loads(resp), {'result': {'success': 1}, 'id': 0, 'jsonrpc': '2.0'})
+ assert json.loads(resp) == {'result': {'success': 1}, 'id': 0, 'jsonrpc': '2.0'}
finally:
end_event.set()
thread.join()
@@ -427,8 +421,4 @@ class TestAthenadMethods(unittest.TestCase):
# ensure the list is all logs except most recent
sl = athenad.get_logs_to_send_sorted()
- self.assertListEqual(sl, fl[:-1])
-
-
-if __name__ == '__main__':
- unittest.main()
+ assert sl == fl[:-1]
diff --git a/selfdrive/athena/tests/test_athenad_ping.py b/system/athena/tests/test_athenad_ping.py
similarity index 65%
rename from selfdrive/athena/tests/test_athenad_ping.py
rename to system/athena/tests/test_athenad_ping.py
index f56fcac8b5..4fda13dfe8 100755
--- a/selfdrive/athena/tests/test_athenad_ping.py
+++ b/system/athena/tests/test_athenad_ping.py
@@ -1,15 +1,14 @@
#!/usr/bin/env python3
+import pytest
import subprocess
import threading
import time
-import unittest
from typing import cast
-from unittest import mock
from openpilot.common.params import Params
from openpilot.common.timeout import Timeout
-from openpilot.selfdrive.athena import athenad
-from openpilot.selfdrive.manager.helpers import write_onroad_params
+from openpilot.system.athena import athenad
+from openpilot.system.manager.helpers import write_onroad_params
from openpilot.system.hardware import TICI
TIMEOUT_TOLERANCE = 20 # seconds
@@ -22,7 +21,7 @@ def wifi_radio(on: bool) -> None:
subprocess.run(["nmcli", "radio", "wifi", "on" if on else "off"], check=True)
-class TestAthenadPing(unittest.TestCase):
+class TestAthenadPing:
params: Params
dongle_id: str
@@ -39,10 +38,10 @@ class TestAthenadPing(unittest.TestCase):
return self._get_ping_time() is not None
@classmethod
- def tearDownClass(cls) -> None:
+ def teardown_class(cls) -> None:
wifi_radio(True)
- def setUp(self) -> None:
+ def setup_method(self) -> None:
self.params = Params()
self.dongle_id = self.params.get("DongleId", encoding="utf-8")
@@ -52,21 +51,23 @@ class TestAthenadPing(unittest.TestCase):
self.exit_event = threading.Event()
self.athenad = threading.Thread(target=athenad.main, args=(self.exit_event,))
- def tearDown(self) -> None:
+ def teardown_method(self) -> None:
if self.athenad.is_alive():
self.exit_event.set()
self.athenad.join()
- @mock.patch('openpilot.selfdrive.athena.athenad.create_connection', new_callable=lambda: mock.MagicMock(wraps=athenad.create_connection))
- def assertTimeout(self, reconnect_time: float, mock_create_connection: mock.MagicMock) -> None:
+ def assertTimeout(self, reconnect_time: float, subtests, mocker) -> None:
self.athenad.start()
+ mock_create_connection = mocker.patch('openpilot.system.athena.athenad.create_connection',
+ new_callable=lambda: mocker.MagicMock(wraps=athenad.create_connection))
+
time.sleep(1)
mock_create_connection.assert_called_once()
mock_create_connection.reset_mock()
# check normal behaviour, server pings on connection
- with self.subTest("Wi-Fi: receives ping"), Timeout(70, "no ping received"):
+ with subtests.test("Wi-Fi: receives ping"), Timeout(70, "no ping received"):
while not self._received_ping():
time.sleep(0.1)
print("ping received")
@@ -74,7 +75,7 @@ class TestAthenadPing(unittest.TestCase):
mock_create_connection.assert_not_called()
# websocket should attempt reconnect after short time
- with self.subTest("LTE: attempt reconnect"):
+ with subtests.test("LTE: attempt reconnect"):
wifi_radio(False)
print("waiting for reconnect attempt")
start_time = time.monotonic()
@@ -86,21 +87,17 @@ class TestAthenadPing(unittest.TestCase):
self._clear_ping_time()
# check ping received after reconnect
- with self.subTest("LTE: receives ping"), Timeout(70, "no ping received"):
+ with subtests.test("LTE: receives ping"), Timeout(70, "no ping received"):
while not self._received_ping():
time.sleep(0.1)
print("ping received")
- @unittest.skipIf(not TICI, "only run on desk")
- def test_offroad(self) -> None:
+ @pytest.mark.skipif(not TICI, reason="only run on desk")
+ def test_offroad(self, subtests, mocker) -> None:
write_onroad_params(False, self.params)
- self.assertTimeout(60 + TIMEOUT_TOLERANCE) # based using TCP keepalive settings
+ self.assertTimeout(60 + TIMEOUT_TOLERANCE, subtests, mocker) # based using TCP keepalive settings
- @unittest.skipIf(not TICI, "only run on desk")
- def test_onroad(self) -> None:
+ @pytest.mark.skipif(not TICI, reason="only run on desk")
+ def test_onroad(self, subtests, mocker) -> None:
write_onroad_params(True, self.params)
- self.assertTimeout(21 + TIMEOUT_TOLERANCE)
-
-
-if __name__ == "__main__":
- unittest.main()
+ self.assertTimeout(21 + TIMEOUT_TOLERANCE, subtests, mocker)
diff --git a/system/athena/tests/test_registration.py b/system/athena/tests/test_registration.py
new file mode 100755
index 0000000000..85c065c1bf
--- /dev/null
+++ b/system/athena/tests/test_registration.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+import json
+from Crypto.PublicKey import RSA
+from pathlib import Path
+
+from openpilot.common.params import Params
+from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_ID
+from openpilot.system.athena.tests.helpers import MockResponse
+from openpilot.system.hardware.hw import Paths
+
+
+class TestRegistration:
+
+ def setup_method(self):
+ # clear params and setup key paths
+ self.params = Params()
+ self.params.clear_all()
+
+ persist_dir = Path(Paths.persist_root()) / "comma"
+ persist_dir.mkdir(parents=True, exist_ok=True)
+
+ self.priv_key = persist_dir / "id_rsa"
+ self.pub_key = persist_dir / "id_rsa.pub"
+
+ def _generate_keys(self):
+ self.pub_key.touch()
+ k = RSA.generate(2048)
+ with open(self.priv_key, "wb") as f:
+ f.write(k.export_key())
+ with open(self.pub_key, "wb") as f:
+ f.write(k.publickey().export_key())
+
+ def test_valid_cache(self, mocker):
+ # if all params are written, return the cached dongle id
+ self.params.put("IMEI", "imei")
+ self.params.put("HardwareSerial", "serial")
+ self._generate_keys()
+
+ m = mocker.patch("openpilot.system.athena.registration.api_get", autospec=True)
+ dongle = "DONGLE_ID_123"
+ self.params.put("DongleId", dongle)
+ assert register() == dongle
+ assert not m.called
+
+ def test_no_keys(self, mocker):
+ # missing pubkey
+ m = mocker.patch("openpilot.system.athena.registration.api_get", autospec=True)
+ dongle = register()
+ assert m.call_count == 0
+ assert dongle == UNREGISTERED_DONGLE_ID
+ assert self.params.get("DongleId", encoding='utf-8') == dongle
+
+ def test_missing_cache(self, mocker):
+ # keys exist but no dongle id
+ self._generate_keys()
+ m = mocker.patch("openpilot.system.athena.registration.api_get", autospec=True)
+ dongle = "DONGLE_ID_123"
+ m.return_value = MockResponse(json.dumps({'dongle_id': dongle}), 200)
+ assert register() == dongle
+ assert m.call_count == 1
+
+ # call again, shouldn't hit the API this time
+ assert register() == dongle
+ assert m.call_count == 1
+ assert self.params.get("DongleId", encoding='utf-8') == dongle
+
+ def test_unregistered(self, mocker):
+ # keys exist, but unregistered
+ self._generate_keys()
+ m = mocker.patch("openpilot.system.athena.registration.api_get", autospec=True)
+ m.return_value = MockResponse(None, 402)
+ dongle = register()
+ assert m.call_count == 1
+ assert dongle == UNREGISTERED_DONGLE_ID
+ assert self.params.get("DongleId", encoding='utf-8') == dongle
diff --git a/system/camerad/snapshot/snapshot.py b/system/camerad/snapshot/snapshot.py
index 8c1b6084c7..d9377eebe9 100755
--- a/system/camerad/snapshot/snapshot.py
+++ b/system/camerad/snapshot/snapshot.py
@@ -11,7 +11,7 @@ from openpilot.common.params import Params
from openpilot.common.realtime import DT_MDL
from openpilot.system.hardware import PC
from openpilot.selfdrive.controls.lib.alertmanager import set_offroad_alert
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
VISION_STREAMS = {
diff --git a/system/camerad/test/test_camerad.py b/system/camerad/test/test_camerad.py
index 408115607e..dcc9825632 100755
--- a/system/camerad/test/test_camerad.py
+++ b/system/camerad/test/test_camerad.py
@@ -8,7 +8,7 @@ from collections import defaultdict
import cereal.messaging as messaging
from cereal import log
from cereal.services import SERVICE_LIST
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
TEST_TIMESPAN = 30
LAG_FRAME_TOLERANCE = {log.FrameData.ImageSensor.ar0231: 0.5, # ARs use synced pulses for frame starts
diff --git a/system/camerad/test/test_exposure.py b/system/camerad/test/test_exposure.py
index 50467f9db4..36e8522b1d 100755
--- a/system/camerad/test/test_exposure.py
+++ b/system/camerad/test/test_exposure.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import time
-import unittest
import numpy as np
from openpilot.selfdrive.test.helpers import with_processes, phone_only
@@ -9,9 +8,9 @@ from openpilot.system.camerad.snapshot.snapshot import get_snapshots
TEST_TIME = 45
REPEAT = 5
-class TestCamerad(unittest.TestCase):
+class TestCamerad:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
pass
def _numpy_rgb2gray(self, im):
@@ -49,7 +48,4 @@ class TestCamerad(unittest.TestCase):
passed += int(res)
time.sleep(2)
- self.assertGreaterEqual(passed, REPEAT)
-
-if __name__ == "__main__":
- unittest.main()
+ assert passed >= REPEAT
diff --git a/system/hardware/tici/tests/test_agnos_updater.py b/system/hardware/tici/tests/test_agnos_updater.py
index 86bc78881e..462cf6cb5c 100755
--- a/system/hardware/tici/tests/test_agnos_updater.py
+++ b/system/hardware/tici/tests/test_agnos_updater.py
@@ -1,14 +1,13 @@
#!/usr/bin/env python3
import json
import os
-import unittest
import requests
TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
MANIFEST = os.path.join(TEST_DIR, "../agnos.json")
-class TestAgnosUpdater(unittest.TestCase):
+class TestAgnosUpdater:
def test_manifest(self):
with open(MANIFEST) as f:
@@ -17,10 +16,6 @@ class TestAgnosUpdater(unittest.TestCase):
for img in m:
r = requests.head(img['url'], timeout=10)
r.raise_for_status()
- self.assertEqual(r.headers['Content-Type'], "application/x-xz")
+ assert r.headers['Content-Type'] == "application/x-xz"
if not img['sparse']:
assert img['hash'] == img['hash_raw']
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/system/hardware/tici/tests/test_amplifier.py b/system/hardware/tici/tests/test_amplifier.py
index cd3b0f90fe..dfba84b942 100755
--- a/system/hardware/tici/tests/test_amplifier.py
+++ b/system/hardware/tici/tests/test_amplifier.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
+import pytest
import time
import random
-import unittest
import subprocess
from panda import Panda
@@ -10,14 +10,14 @@ from openpilot.system.hardware.tici.hardware import Tici
from openpilot.system.hardware.tici.amplifier import Amplifier
-class TestAmplifier(unittest.TestCase):
+class TestAmplifier:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
if not TICI:
- raise unittest.SkipTest
+ pytest.skip()
- def setUp(self):
+ def setup_method(self):
# clear dmesg
subprocess.check_call("sudo dmesg -C", shell=True)
@@ -25,7 +25,7 @@ class TestAmplifier(unittest.TestCase):
Panda.wait_for_panda(None, 30)
self.panda = Panda()
- def tearDown(self):
+ def teardown_method(self):
HARDWARE.reset_internal_panda()
def _check_for_i2c_errors(self, expected):
@@ -68,8 +68,4 @@ class TestAmplifier(unittest.TestCase):
if self._check_for_i2c_errors(True):
break
else:
- self.fail("didn't hit any i2c errors")
-
-
-if __name__ == "__main__":
- unittest.main()
+ pytest.fail("didn't hit any i2c errors")
diff --git a/system/hardware/tici/tests/test_hardware.py b/system/hardware/tici/tests/test_hardware.py
index 6c41c383a0..49d4ac7699 100755
--- a/system/hardware/tici/tests/test_hardware.py
+++ b/system/hardware/tici/tests/test_hardware.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import pytest
import time
-import unittest
import numpy as np
from openpilot.system.hardware.tici.hardware import Tici
@@ -10,7 +9,7 @@ HARDWARE = Tici()
@pytest.mark.tici
-class TestHardware(unittest.TestCase):
+class TestHardware:
def test_power_save_time(self):
ts = []
@@ -22,7 +21,3 @@ class TestHardware(unittest.TestCase):
assert 0.1 < np.mean(ts) < 0.25
assert max(ts) < 0.3
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/system/hardware/tici/tests/test_power_draw.py b/system/hardware/tici/tests/test_power_draw.py
index ba7e0a6d9d..1fe063c8e8 100755
--- a/system/hardware/tici/tests/test_power_draw.py
+++ b/system/hardware/tici/tests/test_power_draw.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
from collections import defaultdict, deque
import pytest
-import unittest
import time
import numpy as np
from dataclasses import dataclass
@@ -12,8 +11,8 @@ from cereal.services import SERVICE_LIST
from openpilot.common.mock import mock_messages
from openpilot.selfdrive.car.car_helpers import write_car_param
from openpilot.system.hardware.tici.power_monitor import get_power
-from openpilot.selfdrive.manager.process_config import managed_processes
-from openpilot.selfdrive.manager.manager import manager_cleanup
+from openpilot.system.manager.process_config import managed_processes
+from openpilot.system.manager.manager import manager_cleanup
SAMPLE_TIME = 8 # seconds to sample power
MAX_WARMUP_TIME = 30 # seconds to wait for SAMPLE_TIME consecutive valid samples
@@ -40,15 +39,15 @@ PROCS = [
@pytest.mark.tici
-class TestPowerDraw(unittest.TestCase):
+class TestPowerDraw:
- def setUp(self):
+ def setup_method(self):
write_car_param()
# wait a bit for power save to disable
time.sleep(5)
- def tearDown(self):
+ def teardown_method(self):
manager_cleanup()
def get_expected_messages(self, proc):
@@ -97,7 +96,7 @@ class TestPowerDraw(unittest.TestCase):
return now, msg_counts, time.monotonic() - start_time - SAMPLE_TIME
@mock_messages(['liveLocationKalman'])
- def test_camera_procs(self):
+ def test_camera_procs(self, subtests):
baseline = get_power()
prev = baseline
@@ -122,12 +121,8 @@ class TestPowerDraw(unittest.TestCase):
expected = proc.power
msgs_received = sum(msg_counts[msg] for msg in proc.msgs)
tab.append([proc.name, round(expected, 2), round(cur, 2), self.get_expected_messages(proc), msgs_received, round(warmup_time[proc.name], 2)])
- with self.subTest(proc=proc.name):
- self.assertTrue(self.valid_msg_count(proc, msg_counts), f"expected {self.get_expected_messages(proc)} msgs, got {msgs_received} msgs")
- self.assertTrue(self.valid_power_draw(proc, cur), f"expected {expected:.2f}W, got {cur:.2f}W")
+ with subtests.test(proc=proc.name):
+ assert self.valid_msg_count(proc, msg_counts), f"expected {self.get_expected_messages(proc)} msgs, got {msgs_received} msgs"
+ assert self.valid_power_draw(proc, cur), f"expected {expected:.2f}W, got {cur:.2f}W"
print(tabulate(tab))
print(f"Baseline {baseline:.2f}W\n")
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/system/loggerd/encoder/encoder.cc b/system/loggerd/encoder/encoder.cc
index c4bd91bcf7..313a0f57a1 100644
--- a/system/loggerd/encoder/encoder.cc
+++ b/system/loggerd/encoder/encoder.cc
@@ -6,7 +6,12 @@ VideoEncoder::VideoEncoder(const EncoderInfo &encoder_info, int in_width, int in
out_width = encoder_info.frame_width > 0 ? encoder_info.frame_width : in_width;
out_height = encoder_info.frame_height > 0 ? encoder_info.frame_height : in_height;
- pm.reset(new PubMaster({encoder_info.publish_name}));
+
+ std::vector pubs = {encoder_info.publish_name};
+ if (encoder_info.thumbnail_name != NULL) {
+ pubs.push_back(encoder_info.thumbnail_name);
+ }
+ pm.reset(new PubMaster(pubs));
}
void VideoEncoder::publisher_publish(VideoEncoder *e, int segment_num, uint32_t idx, VisionIpcBufExtra &extra,
@@ -40,4 +45,15 @@ void VideoEncoder::publisher_publish(VideoEncoder *e, int segment_num, uint32_t
kj::ArrayOutputStream output_stream(kj::ArrayPtr(e->msg_cache.data(), bytes_size));
capnp::writeMessage(output_stream, msg);
e->pm->send(e->encoder_info.publish_name, e->msg_cache.data(), bytes_size);
-}
+
+ // Publish keyframe thumbnail
+ if ((flags & V4L2_BUF_FLAG_KEYFRAME) && e->encoder_info.thumbnail_name != NULL) {
+ MessageBuilder tm;
+ auto thumbnail = tm.initEvent().initThumbnail();
+ thumbnail.setFrameId(extra.frame_id);
+ thumbnail.setTimestampEof(extra.timestamp_eof);
+ thumbnail.setThumbnail(dat);
+ thumbnail.setEncoding(cereal::Thumbnail::Encoding::KEYFRAME);
+ pm->send(e->encoder_info.thumbnail_name, tm);
+ }
+}
\ No newline at end of file
diff --git a/system/loggerd/encoder/encoder.h b/system/loggerd/encoder/encoder.h
index 7c203f9193..75183611b3 100644
--- a/system/loggerd/encoder/encoder.h
+++ b/system/loggerd/encoder/encoder.h
@@ -25,6 +25,8 @@ public:
void publisher_publish(VideoEncoder *e, int segment_num, uint32_t idx, VisionIpcBufExtra &extra, unsigned int flags, kj::ArrayPtr header, kj::ArrayPtr dat);
protected:
+ void publish_thumbnail(uint32_t frame_id, uint64_t timestamp_eof, kj::ArrayPtr dat);
+
int in_width, in_height;
int out_width, out_height;
const EncoderInfo encoder_info;
diff --git a/system/loggerd/loggerd.h b/system/loggerd/loggerd.h
index ea288f4861..5eef161834 100644
--- a/system/loggerd/loggerd.h
+++ b/system/loggerd/loggerd.h
@@ -33,6 +33,7 @@ constexpr char PRESERVE_ATTR_VALUE = '1';
class EncoderInfo {
public:
const char *publish_name;
+ const char *thumbnail_name = NULL;
const char *filename = NULL;
bool record = true;
int frame_width = -1;
@@ -76,6 +77,7 @@ const EncoderInfo main_driver_encoder_info = {
const EncoderInfo stream_road_encoder_info = {
.publish_name = "livestreamRoadEncodeData",
+ //.thumbnail_name = "thumbnail",
.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264,
.record = false,
.bitrate = LIVESTREAM_BITRATE,
diff --git a/system/loggerd/tests/loggerd_tests_common.py b/system/loggerd/tests/loggerd_tests_common.py
index 877c872b6b..15fd997eb8 100644
--- a/system/loggerd/tests/loggerd_tests_common.py
+++ b/system/loggerd/tests/loggerd_tests_common.py
@@ -1,6 +1,5 @@
import os
import random
-import unittest
from pathlib import Path
@@ -29,12 +28,12 @@ def create_random_file(file_path: Path, size_mb: float, lock: bool = False, uplo
if upload_xattr is not None:
setxattr(str(file_path), uploader.UPLOAD_ATTR_NAME, upload_xattr)
-class MockResponse():
+class MockResponse:
def __init__(self, text, status_code):
self.text = text
self.status_code = status_code
-class MockApi():
+class MockApi:
def __init__(self, dongle_id):
pass
@@ -44,7 +43,7 @@ class MockApi():
def get_token(self):
return "fake-token"
-class MockApiIgnore():
+class MockApiIgnore:
def __init__(self, dongle_id):
pass
@@ -54,7 +53,7 @@ class MockApiIgnore():
def get_token(self):
return "fake-token"
-class UploaderTestCase(unittest.TestCase):
+class UploaderTestCase:
f_type = "UNKNOWN"
root: Path
@@ -66,7 +65,7 @@ class UploaderTestCase(unittest.TestCase):
def set_ignore(self):
uploader.Api = MockApiIgnore
- def setUp(self):
+ def setup_method(self):
uploader.Api = MockApi
uploader.fake_upload = True
uploader.force_wifi = True
diff --git a/system/loggerd/tests/test_deleter.py b/system/loggerd/tests/test_deleter.py
index 37d25507e0..3ba6ad4031 100755
--- a/system/loggerd/tests/test_deleter.py
+++ b/system/loggerd/tests/test_deleter.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import time
import threading
-import unittest
from collections import namedtuple
from pathlib import Path
from collections.abc import Sequence
@@ -17,9 +16,9 @@ class TestDeleter(UploaderTestCase):
def fake_statvfs(self, d):
return self.fake_stats
- def setUp(self):
+ def setup_method(self):
self.f_type = "fcamera.hevc"
- super().setUp()
+ super().setup_method()
self.fake_stats = Stats(f_bavail=0, f_blocks=10, f_frsize=4096)
deleter.os.statvfs = self.fake_statvfs
@@ -64,7 +63,7 @@ class TestDeleter(UploaderTestCase):
finally:
self.join_thread()
- self.assertEqual(deleted_order, f_paths, "Files not deleted in expected order")
+ assert deleted_order == f_paths, "Files not deleted in expected order"
def test_delete_order(self):
self.assertDeleteOrder([
@@ -105,7 +104,7 @@ class TestDeleter(UploaderTestCase):
time.sleep(0.01)
self.join_thread()
- self.assertTrue(f_path.exists(), "File deleted with available space")
+ assert f_path.exists(), "File deleted with available space"
def test_no_delete_with_lock_file(self):
f_path = self.make_file_with_data(self.seg_dir, self.f_type, lock=True)
@@ -116,8 +115,4 @@ class TestDeleter(UploaderTestCase):
time.sleep(0.01)
self.join_thread()
- self.assertTrue(f_path.exists(), "File deleted when locked")
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert f_path.exists(), "File deleted when locked"
diff --git a/system/loggerd/tests/test_encoder.py b/system/loggerd/tests/test_encoder.py
index bd076dc5f3..ddf074d3b5 100755
--- a/system/loggerd/tests/test_encoder.py
+++ b/system/loggerd/tests/test_encoder.py
@@ -6,7 +6,6 @@ import random
import shutil
import subprocess
import time
-import unittest
from pathlib import Path
from parameterized import parameterized
@@ -15,7 +14,7 @@ from tqdm import trange
from openpilot.common.params import Params
from openpilot.common.timeout import Timeout
from openpilot.system.hardware import TICI
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
from openpilot.tools.lib.logreader import LogReader
from openpilot.system.hardware.hw import Paths
@@ -33,14 +32,14 @@ FILE_SIZE_TOLERANCE = 0.5
@pytest.mark.tici # TODO: all of loggerd should work on PC
-class TestEncoder(unittest.TestCase):
+class TestEncoder:
- def setUp(self):
+ def setup_method(self):
self._clear_logs()
os.environ["LOGGERD_TEST"] = "1"
os.environ["LOGGERD_SEGMENT_LENGTH"] = str(SEGMENT_LENGTH)
- def tearDown(self):
+ def teardown_method(self):
self._clear_logs()
def _clear_logs(self):
@@ -85,7 +84,7 @@ class TestEncoder(unittest.TestCase):
file_path = f"{route_prefix_path}--{i}/{camera}"
# check file exists
- self.assertTrue(os.path.exists(file_path), f"segment #{i}: '{file_path}' missing")
+ assert os.path.exists(file_path), f"segment #{i}: '{file_path}' missing"
# TODO: this ffprobe call is really slow
# check frame count
@@ -98,13 +97,13 @@ class TestEncoder(unittest.TestCase):
frame_count = int(probe.split('\n')[0].strip())
counts.append(frame_count)
- self.assertEqual(frame_count, expected_frames,
- f"segment #{i}: {camera} failed frame count check: expected {expected_frames}, got {frame_count}")
+ assert frame_count == expected_frames, \
+ f"segment #{i}: {camera} failed frame count check: expected {expected_frames}, got {frame_count}"
# sanity check file size
file_size = os.path.getsize(file_path)
- self.assertTrue(math.isclose(file_size, size, rel_tol=FILE_SIZE_TOLERANCE),
- f"{file_path} size {file_size} isn't close to target size {size}")
+ assert math.isclose(file_size, size, rel_tol=FILE_SIZE_TOLERANCE), \
+ f"{file_path} size {file_size} isn't close to target size {size}"
# Check encodeIdx
if encode_idx_name is not None:
@@ -118,24 +117,24 @@ class TestEncoder(unittest.TestCase):
frame_idxs = [m.frameId for m in encode_msgs]
# Check frame count
- self.assertEqual(frame_count, len(segment_idxs))
- self.assertEqual(frame_count, len(encode_idxs))
+ assert frame_count == len(segment_idxs)
+ assert frame_count == len(encode_idxs)
# Check for duplicates or skips
- self.assertEqual(0, segment_idxs[0])
- self.assertEqual(len(set(segment_idxs)), len(segment_idxs))
+ assert 0 == segment_idxs[0]
+ assert len(set(segment_idxs)) == len(segment_idxs)
- self.assertTrue(all(valid))
+ assert all(valid)
- self.assertEqual(expected_frames * i, encode_idxs[0])
+ assert expected_frames * i == encode_idxs[0]
first_frames.append(frame_idxs[0])
- self.assertEqual(len(set(encode_idxs)), len(encode_idxs))
+ assert len(set(encode_idxs)) == len(encode_idxs)
- self.assertEqual(1, len(set(first_frames)))
+ assert 1 == len(set(first_frames))
if TICI:
expected_frames = fps * SEGMENT_LENGTH
- self.assertEqual(min(counts), expected_frames)
+ assert min(counts) == expected_frames
shutil.rmtree(f"{route_prefix_path}--{i}")
try:
@@ -150,7 +149,3 @@ class TestEncoder(unittest.TestCase):
managed_processes['encoderd'].stop()
managed_processes['camerad'].stop()
managed_processes['sensord'].stop()
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/system/loggerd/tests/test_loggerd.py b/system/loggerd/tests/test_loggerd.py
index fdea60a282..d3789badb7 100755
--- a/system/loggerd/tests/test_loggerd.py
+++ b/system/loggerd/tests/test_loggerd.py
@@ -19,7 +19,7 @@ from openpilot.common.timeout import Timeout
from openpilot.system.hardware.hw import Paths
from openpilot.system.loggerd.xattr_cache import getxattr
from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_VALUE
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
from openpilot.system.version import get_version
from openpilot.tools.lib.helpers import RE
from openpilot.tools.lib.logreader import LogReader
diff --git a/system/loggerd/tests/test_uploader.py b/system/loggerd/tests/test_uploader.py
index 73917a30cf..c0a9770e53 100755
--- a/system/loggerd/tests/test_uploader.py
+++ b/system/loggerd/tests/test_uploader.py
@@ -2,7 +2,6 @@
import os
import time
import threading
-import unittest
import logging
import json
from pathlib import Path
@@ -38,8 +37,8 @@ cloudlog.addHandler(log_handler)
class TestUploader(UploaderTestCase):
- def setUp(self):
- super().setUp()
+ def setup_method(self):
+ super().setup_method()
log_handler.reset()
def start_thread(self):
@@ -80,13 +79,13 @@ class TestUploader(UploaderTestCase):
exp_order = self.gen_order([self.seg_num], [])
- self.assertTrue(len(log_handler.upload_ignored) == 0, "Some files were ignored")
- self.assertFalse(len(log_handler.upload_order) < len(exp_order), "Some files failed to upload")
- self.assertFalse(len(log_handler.upload_order) > len(exp_order), "Some files were uploaded twice")
+ assert len(log_handler.upload_ignored) == 0, "Some files were ignored"
+ assert not len(log_handler.upload_order) < len(exp_order), "Some files failed to upload"
+ assert not len(log_handler.upload_order) > len(exp_order), "Some files were uploaded twice"
for f_path in exp_order:
- self.assertEqual(os.getxattr((Path(Paths.log_root()) / f_path).with_suffix(""), UPLOAD_ATTR_NAME), UPLOAD_ATTR_VALUE, "All files not uploaded")
+ assert os.getxattr((Path(Paths.log_root()) / f_path).with_suffix(""), UPLOAD_ATTR_NAME) == UPLOAD_ATTR_VALUE, "All files not uploaded"
- self.assertTrue(log_handler.upload_order == exp_order, "Files uploaded in wrong order")
+ assert log_handler.upload_order == exp_order, "Files uploaded in wrong order"
def test_upload_with_wrong_xattr(self):
self.gen_files(lock=False, xattr=b'0')
@@ -98,13 +97,13 @@ class TestUploader(UploaderTestCase):
exp_order = self.gen_order([self.seg_num], [])
- self.assertTrue(len(log_handler.upload_ignored) == 0, "Some files were ignored")
- self.assertFalse(len(log_handler.upload_order) < len(exp_order), "Some files failed to upload")
- self.assertFalse(len(log_handler.upload_order) > len(exp_order), "Some files were uploaded twice")
+ assert len(log_handler.upload_ignored) == 0, "Some files were ignored"
+ assert not len(log_handler.upload_order) < len(exp_order), "Some files failed to upload"
+ assert not len(log_handler.upload_order) > len(exp_order), "Some files were uploaded twice"
for f_path in exp_order:
- self.assertEqual(os.getxattr((Path(Paths.log_root()) / f_path).with_suffix(""), UPLOAD_ATTR_NAME), UPLOAD_ATTR_VALUE, "All files not uploaded")
+ assert os.getxattr((Path(Paths.log_root()) / f_path).with_suffix(""), UPLOAD_ATTR_NAME) == UPLOAD_ATTR_VALUE, "All files not uploaded"
- self.assertTrue(log_handler.upload_order == exp_order, "Files uploaded in wrong order")
+ assert log_handler.upload_order == exp_order, "Files uploaded in wrong order"
def test_upload_ignored(self):
self.set_ignore()
@@ -117,13 +116,13 @@ class TestUploader(UploaderTestCase):
exp_order = self.gen_order([self.seg_num], [])
- self.assertTrue(len(log_handler.upload_order) == 0, "Some files were not ignored")
- self.assertFalse(len(log_handler.upload_ignored) < len(exp_order), "Some files failed to ignore")
- self.assertFalse(len(log_handler.upload_ignored) > len(exp_order), "Some files were ignored twice")
+ assert len(log_handler.upload_order) == 0, "Some files were not ignored"
+ assert not len(log_handler.upload_ignored) < len(exp_order), "Some files failed to ignore"
+ assert not len(log_handler.upload_ignored) > len(exp_order), "Some files were ignored twice"
for f_path in exp_order:
- self.assertEqual(os.getxattr((Path(Paths.log_root()) / f_path).with_suffix(""), UPLOAD_ATTR_NAME), UPLOAD_ATTR_VALUE, "All files not ignored")
+ assert os.getxattr((Path(Paths.log_root()) / f_path).with_suffix(""), UPLOAD_ATTR_NAME) == UPLOAD_ATTR_VALUE, "All files not ignored"
- self.assertTrue(log_handler.upload_ignored == exp_order, "Files ignored in wrong order")
+ assert log_handler.upload_ignored == exp_order, "Files ignored in wrong order"
def test_upload_files_in_create_order(self):
seg1_nums = [0, 1, 2, 10, 20]
@@ -142,13 +141,13 @@ class TestUploader(UploaderTestCase):
time.sleep(5)
self.join_thread()
- self.assertTrue(len(log_handler.upload_ignored) == 0, "Some files were ignored")
- self.assertFalse(len(log_handler.upload_order) < len(exp_order), "Some files failed to upload")
- self.assertFalse(len(log_handler.upload_order) > len(exp_order), "Some files were uploaded twice")
+ assert len(log_handler.upload_ignored) == 0, "Some files were ignored"
+ assert not len(log_handler.upload_order) < len(exp_order), "Some files failed to upload"
+ assert not len(log_handler.upload_order) > len(exp_order), "Some files were uploaded twice"
for f_path in exp_order:
- self.assertEqual(os.getxattr((Path(Paths.log_root()) / f_path).with_suffix(""), UPLOAD_ATTR_NAME), UPLOAD_ATTR_VALUE, "All files not uploaded")
+ assert os.getxattr((Path(Paths.log_root()) / f_path).with_suffix(""), UPLOAD_ATTR_NAME) == UPLOAD_ATTR_VALUE, "All files not uploaded"
- self.assertTrue(log_handler.upload_order == exp_order, "Files uploaded in wrong order")
+ assert log_handler.upload_order == exp_order, "Files uploaded in wrong order"
def test_no_upload_with_lock_file(self):
self.start_thread()
@@ -163,7 +162,7 @@ class TestUploader(UploaderTestCase):
for f_path in f_paths:
fn = f_path.with_suffix(f_path.suffix.replace(".bz2", ""))
uploaded = UPLOAD_ATTR_NAME in os.listxattr(fn) and os.getxattr(fn, UPLOAD_ATTR_NAME) == UPLOAD_ATTR_VALUE
- self.assertFalse(uploaded, "File upload when locked")
+ assert not uploaded, "File upload when locked"
def test_no_upload_with_xattr(self):
self.gen_files(lock=False, xattr=UPLOAD_ATTR_VALUE)
@@ -173,7 +172,7 @@ class TestUploader(UploaderTestCase):
time.sleep(5)
self.join_thread()
- self.assertEqual(len(log_handler.upload_order), 0, "File uploaded again")
+ assert len(log_handler.upload_order) == 0, "File uploaded again"
def test_clear_locks_on_startup(self):
f_paths = self.gen_files(lock=True, boot=False)
@@ -183,8 +182,4 @@ class TestUploader(UploaderTestCase):
for f_path in f_paths:
lock_path = f_path.with_suffix(f_path.suffix + ".lock")
- self.assertFalse(lock_path.is_file(), "File lock not cleared on startup")
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert not lock_path.is_file(), "File lock not cleared on startup"
diff --git a/selfdrive/manager/__init__.py b/system/manager/__init__.py
similarity index 100%
rename from selfdrive/manager/__init__.py
rename to system/manager/__init__.py
diff --git a/selfdrive/manager/build.py b/system/manager/build.py
similarity index 99%
rename from selfdrive/manager/build.py
rename to system/manager/build.py
index 859d0920c3..ac7822d51b 100755
--- a/selfdrive/manager/build.py
+++ b/system/manager/build.py
@@ -14,7 +14,7 @@ from openpilot.system.version import get_build_metadata
MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9
CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache")
-TOTAL_SCONS_NODES = 2560
+TOTAL_SCONS_NODES = 2410
MAX_BUILD_PROGRESS = 100
def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
diff --git a/selfdrive/manager/helpers.py b/system/manager/helpers.py
similarity index 100%
rename from selfdrive/manager/helpers.py
rename to system/manager/helpers.py
diff --git a/selfdrive/manager/manager.py b/system/manager/manager.py
similarity index 96%
rename from selfdrive/manager/manager.py
rename to system/manager/manager.py
index 53826d72ba..6dad190ec9 100755
--- a/selfdrive/manager/manager.py
+++ b/system/manager/manager.py
@@ -7,14 +7,14 @@ import traceback
from cereal import log
import cereal.messaging as messaging
-import openpilot.selfdrive.sentry as sentry
+import openpilot.system.sentry as sentry
from openpilot.common.params import Params, ParamKeyType
from openpilot.common.text_window import TextWindow
from openpilot.system.hardware import HARDWARE, PC
-from openpilot.selfdrive.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog
-from openpilot.selfdrive.manager.process import ensure_running
-from openpilot.selfdrive.manager.process_config import managed_processes
-from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID
+from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog
+from openpilot.system.manager.process import ensure_running
+from openpilot.system.manager.process_config import managed_processes
+from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_ID
from openpilot.common.swaglog import cloudlog, add_file_handler
from openpilot.system.version import get_build_metadata, terms_version, training_version
diff --git a/selfdrive/manager/process.py b/system/manager/process.py
similarity index 99%
rename from selfdrive/manager/process.py
rename to system/manager/process.py
index 7964f5229d..36f299ae62 100644
--- a/selfdrive/manager/process.py
+++ b/system/manager/process.py
@@ -12,7 +12,7 @@ from setproctitle import setproctitle
from cereal import car, log
import cereal.messaging as messaging
-import openpilot.selfdrive.sentry as sentry
+import openpilot.system.sentry as sentry
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
diff --git a/selfdrive/manager/process_config.py b/system/manager/process_config.py
similarity index 88%
rename from selfdrive/manager/process_config.py
rename to system/manager/process_config.py
index bdee69120b..0cc8950194 100644
--- a/selfdrive/manager/process_config.py
+++ b/system/manager/process_config.py
@@ -3,7 +3,7 @@ import os
from cereal import car
from openpilot.common.params import Params
from openpilot.system.hardware import PC, TICI
-from openpilot.selfdrive.manager.process import PythonProcess, NativeProcess, DaemonProcess
+from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess
WEBCAM = os.getenv("USE_WEBCAM") is not None
@@ -42,7 +42,7 @@ def only_offroad(started, params, CP: car.CarParams) -> bool:
return not started
procs = [
- DaemonProcess("manage_athenad", "selfdrive.athena.manage_athenad", "AthenadPid"),
+ DaemonProcess("manage_athenad", "system.athena.manage_athenad", "AthenadPid"),
NativeProcess("camerad", "system/camerad", ["./camerad"], driverview),
NativeProcess("logcatd", "system/logcatd", ["./logcatd"], only_onroad),
@@ -64,6 +64,7 @@ procs = [
PythonProcess("calibrationd", "selfdrive.locationd.calibrationd", only_onroad),
PythonProcess("torqued", "selfdrive.locationd.torqued", only_onroad),
PythonProcess("controlsd", "selfdrive.controls.controlsd", only_onroad),
+ PythonProcess("card", "selfdrive.car.card", only_onroad),
PythonProcess("deleter", "system.loggerd.deleter", always_run),
PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(not PC or WEBCAM)),
PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI),
@@ -75,11 +76,11 @@ procs = [
PythonProcess("pigeond", "system.ubloxd.pigeond", ublox, enabled=TICI),
PythonProcess("plannerd", "selfdrive.controls.plannerd", only_onroad),
PythonProcess("radard", "selfdrive.controls.radard", only_onroad),
- PythonProcess("thermald", "selfdrive.thermald.thermald", always_run),
- PythonProcess("tombstoned", "selfdrive.tombstoned", always_run, enabled=not PC),
- PythonProcess("updated", "selfdrive.updated.updated", only_offroad, enabled=not PC),
+ PythonProcess("thermald", "system.thermald.thermald", always_run),
+ PythonProcess("tombstoned", "system.tombstoned", always_run, enabled=not PC),
+ PythonProcess("updated", "system.updated.updated", only_offroad, enabled=not PC),
PythonProcess("uploader", "system.loggerd.uploader", always_run),
- PythonProcess("statsd", "selfdrive.statsd", always_run),
+ PythonProcess("statsd", "system.statsd", always_run),
PythonProcess("fleet_manager", "system.fleetmanager.fleet_manager", always_run), # TODO: Handle sigkill properly
diff --git a/selfdrive/manager/test/__init__.py b/system/manager/test/__init__.py
similarity index 100%
rename from selfdrive/manager/test/__init__.py
rename to system/manager/test/__init__.py
diff --git a/selfdrive/manager/test/test_manager.py b/system/manager/test/test_manager.py
similarity index 64%
rename from selfdrive/manager/test/test_manager.py
rename to system/manager/test/test_manager.py
index 1ae94b26a1..af8397b246 100755
--- a/selfdrive/manager/test/test_manager.py
+++ b/system/manager/test/test_manager.py
@@ -3,15 +3,14 @@ import os
import pytest
import signal
import time
-import unittest
from parameterized import parameterized
from cereal import car
from openpilot.common.params import Params
-import openpilot.selfdrive.manager.manager as manager
-from openpilot.selfdrive.manager.process import ensure_running
-from openpilot.selfdrive.manager.process_config import managed_processes
+import openpilot.system.manager.manager as manager
+from openpilot.system.manager.process import ensure_running
+from openpilot.system.manager.process_config import managed_processes
from openpilot.system.hardware import HARDWARE
os.environ['FAKEUPLOAD'] = "1"
@@ -21,15 +20,15 @@ BLACKLIST_PROCS = ['manage_athenad', 'pandad', 'pigeond']
@pytest.mark.tici
-class TestManager(unittest.TestCase):
- def setUp(self):
+class TestManager:
+ def setup_method(self):
HARDWARE.set_power_save(False)
# ensure clean CarParams
params = Params()
params.clear_all()
- def tearDown(self):
+ def teardown_method(self):
manager.manager_cleanup()
def test_manager_prepare(self):
@@ -38,7 +37,7 @@ class TestManager(unittest.TestCase):
def test_blacklisted_procs(self):
# TODO: ensure there are blacklisted procs until we have a dedicated test
- self.assertTrue(len(BLACKLIST_PROCS), "No blacklisted procs to test not_run")
+ assert len(BLACKLIST_PROCS), "No blacklisted procs to test not_run"
@parameterized.expand([(i,) for i in range(10)])
def test_startup_time(self, index):
@@ -48,8 +47,8 @@ class TestManager(unittest.TestCase):
t = time.monotonic() - start
assert t < MAX_STARTUP_TIME, f"startup took {t}s, expected <{MAX_STARTUP_TIME}s"
- @unittest.skip("this test is flaky the way it's currently written, should be moved to test_onroad")
- def test_clean_exit(self):
+ @pytest.mark.skip("this test is flaky the way it's currently written, should be moved to test_onroad")
+ def test_clean_exit(self, subtests):
"""
Ensure all processes exit cleanly when stopped.
"""
@@ -62,21 +61,17 @@ class TestManager(unittest.TestCase):
time.sleep(10)
for p in procs:
- with self.subTest(proc=p.name):
+ with subtests.test(proc=p.name):
state = p.get_process_state_msg()
- self.assertTrue(state.running, f"{p.name} not running")
+ assert state.running, f"{p.name} not running"
exit_code = p.stop(retry=False)
- self.assertNotIn(p.name, BLACKLIST_PROCS, f"{p.name} was started")
+ assert p.name not in BLACKLIST_PROCS, f"{p.name} was started"
- self.assertTrue(exit_code is not None, f"{p.name} failed to exit")
+ assert exit_code is not None, f"{p.name} failed to exit"
# TODO: interrupted blocking read exits with 1 in cereal. use a more unique return code
exit_codes = [0, 1]
if p.sigkill:
exit_codes = [-signal.SIGKILL]
- self.assertIn(exit_code, exit_codes, f"{p.name} died with {exit_code}")
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert exit_code in exit_codes, f"{p.name} died with {exit_code}"
diff --git a/system/qcomgpsd/tests/test_qcomgpsd.py b/system/qcomgpsd/tests/test_qcomgpsd.py
index 6c93f7dd93..33d6e5b78f 100755
--- a/system/qcomgpsd/tests/test_qcomgpsd.py
+++ b/system/qcomgpsd/tests/test_qcomgpsd.py
@@ -4,35 +4,34 @@ import pytest
import json
import time
import datetime
-import unittest
import subprocess
import cereal.messaging as messaging
from openpilot.system.qcomgpsd.qcomgpsd import at_cmd, wait_for_modem
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
GOOD_SIGNAL = bool(int(os.getenv("GOOD_SIGNAL", '0')))
@pytest.mark.tici
-class TestRawgpsd(unittest.TestCase):
+class TestRawgpsd:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
os.system("sudo systemctl start systemd-resolved")
os.system("sudo systemctl restart ModemManager lte")
wait_for_modem()
@classmethod
- def tearDownClass(cls):
+ def teardown_class(cls):
managed_processes['qcomgpsd'].stop()
os.system("sudo systemctl restart systemd-resolved")
os.system("sudo systemctl restart ModemManager lte")
- def setUp(self):
+ def setup_method(self):
at_cmd("AT+QGPSDEL=0")
self.sm = messaging.SubMaster(['qcomGnss', 'gpsLocation', 'gnssMeasurements'])
- def tearDown(self):
+ def teardown_method(self):
managed_processes['qcomgpsd'].stop()
os.system("sudo systemctl restart systemd-resolved")
@@ -57,18 +56,18 @@ class TestRawgpsd(unittest.TestCase):
os.system("sudo systemctl restart ModemManager")
assert self._wait_for_output(30)
- def test_startup_time(self):
+ def test_startup_time(self, subtests):
for internet in (True, False):
if not internet:
os.system("sudo systemctl stop systemd-resolved")
- with self.subTest(internet=internet):
+ with subtests.test(internet=internet):
managed_processes['qcomgpsd'].start()
assert self._wait_for_output(7)
managed_processes['qcomgpsd'].stop()
- def test_turns_off_gnss(self):
+ def test_turns_off_gnss(self, subtests):
for s in (0.1, 1, 5):
- with self.subTest(runtime=s):
+ with subtests.test(runtime=s):
managed_processes['qcomgpsd'].start()
time.sleep(s)
managed_processes['qcomgpsd'].stop()
@@ -87,7 +86,7 @@ class TestRawgpsd(unittest.TestCase):
if should_be_loaded:
assert valid_duration == "10080" # should be max time
injected_time = datetime.datetime.strptime(injected_time_str.replace("\"", ""), "%Y/%m/%d,%H:%M:%S")
- self.assertLess(abs((datetime.datetime.utcnow() - injected_time).total_seconds()), 60*60*12)
+ assert abs((datetime.datetime.utcnow() - injected_time).total_seconds()) < 60*60*12
else:
valid_duration, injected_time_str = out.split(",", 1)
injected_time_str = injected_time_str.replace('\"', '').replace('\'', '')
@@ -119,6 +118,3 @@ class TestRawgpsd(unittest.TestCase):
time.sleep(15)
managed_processes['qcomgpsd'].stop()
self.check_assistance(True)
-
-if __name__ == "__main__":
- unittest.main(failfast=True)
diff --git a/system/sensord/tests/test_sensord.py b/system/sensord/tests/test_sensord.py
index 3075c8a343..aed3b07b32 100755
--- a/system/sensord/tests/test_sensord.py
+++ b/system/sensord/tests/test_sensord.py
@@ -2,7 +2,6 @@
import os
import pytest
import time
-import unittest
import numpy as np
from collections import namedtuple, defaultdict
@@ -11,7 +10,7 @@ from cereal import log
from cereal.services import SERVICE_LIST
from openpilot.common.gpio import get_irqs_for_action
from openpilot.common.timeout import Timeout
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
BMX = {
('bmx055', 'acceleration'),
@@ -99,9 +98,9 @@ def read_sensor_events(duration_sec):
return {k: v for k, v in events.items() if len(v) > 0}
@pytest.mark.tici
-class TestSensord(unittest.TestCase):
+class TestSensord:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
# enable LSM self test
os.environ["LSM_SELF_TEST"] = "1"
@@ -119,10 +118,10 @@ class TestSensord(unittest.TestCase):
managed_processes["sensord"].stop()
@classmethod
- def tearDownClass(cls):
+ def teardown_class(cls):
managed_processes["sensord"].stop()
- def tearDown(self):
+ def teardown_method(self):
managed_processes["sensord"].stop()
def test_sensors_present(self):
@@ -133,9 +132,9 @@ class TestSensord(unittest.TestCase):
m = getattr(measurement, measurement.which())
seen.add((str(m.source), m.which()))
- self.assertIn(seen, SENSOR_CONFIGURATIONS)
+ assert seen in SENSOR_CONFIGURATIONS
- def test_lsm6ds3_timing(self):
+ def test_lsm6ds3_timing(self, subtests):
# verify measurements are sampled and published at 104Hz
sensor_t = {
@@ -152,7 +151,7 @@ class TestSensord(unittest.TestCase):
sensor_t[m.sensor].append(m.timestamp)
for s, vals in sensor_t.items():
- with self.subTest(sensor=s):
+ with subtests.test(sensor=s):
assert len(vals) > 0
tdiffs = np.diff(vals) / 1e6 # millis
@@ -166,9 +165,9 @@ class TestSensord(unittest.TestCase):
stddev = np.std(tdiffs)
assert stddev < 2.0, f"Standard-dev to big {stddev}"
- def test_sensor_frequency(self):
+ def test_sensor_frequency(self, subtests):
for s, msgs in self.events.items():
- with self.subTest(sensor=s):
+ with subtests.test(sensor=s):
freq = len(msgs) / self.sample_secs
ef = SERVICE_LIST[s].frequency
assert ef*0.85 <= freq <= ef*1.15
@@ -246,6 +245,3 @@ class TestSensord(unittest.TestCase):
state_two = get_irq_count(self.sensord_irq)
assert state_one == state_two, "Interrupts received after sensord stop!"
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/system/sensord/tests/ttff_test.py b/system/sensord/tests/ttff_test.py
index e023489ed5..a9cc16d707 100755
--- a/system/sensord/tests/ttff_test.py
+++ b/system/sensord/tests/ttff_test.py
@@ -4,7 +4,7 @@ import time
import atexit
from cereal import messaging
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
TIMEOUT = 10*60
diff --git a/selfdrive/sentry.py b/system/sentry.py
similarity index 97%
rename from selfdrive/sentry.py
rename to system/sentry.py
index 204d9cab09..5f4772fa71 100644
--- a/selfdrive/sentry.py
+++ b/system/sentry.py
@@ -4,7 +4,7 @@ from enum import Enum
from sentry_sdk.integrations.threading import ThreadingIntegration
from openpilot.common.params import Params
-from openpilot.selfdrive.athena.registration import is_registered_device
+from openpilot.system.athena.registration import is_registered_device
from openpilot.system.hardware import HARDWARE, PC
from openpilot.common.swaglog import cloudlog
from openpilot.system.version import get_build_metadata, get_version
diff --git a/selfdrive/statsd.py b/system/statsd.py
similarity index 97%
rename from selfdrive/statsd.py
rename to system/statsd.py
index 2f9149b096..2b5a7bb3a7 100755
--- a/selfdrive/statsd.py
+++ b/system/statsd.py
@@ -4,7 +4,7 @@ import zmq
import time
from pathlib import Path
from collections import defaultdict
-from datetime import datetime, timezone
+from datetime import datetime, UTC
from typing import NoReturn
from openpilot.common.params import Params
@@ -133,7 +133,7 @@ def main() -> NoReturn:
# flush when started state changes or after FLUSH_TIME_S
if (time.monotonic() > last_flush_time + STATS_FLUSH_TIME_S) or (sm['deviceState'].started != started_prev):
result = ""
- current_time = datetime.utcnow().replace(tzinfo=timezone.utc)
+ current_time = datetime.utcnow().replace(tzinfo=UTC)
tags['started'] = sm['deviceState'].started
for key, value in gauges.items():
diff --git a/system/tests/test_logmessaged.py b/system/tests/test_logmessaged.py
index d27dae46ad..f8c5be09b0 100755
--- a/system/tests/test_logmessaged.py
+++ b/system/tests/test_logmessaged.py
@@ -2,16 +2,15 @@
import glob
import os
import time
-import unittest
import cereal.messaging as messaging
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog, ipchandler
-class TestLogmessaged(unittest.TestCase):
- def setUp(self):
+class TestLogmessaged:
+ def setup_method(self):
# clear the IPC buffer in case some other tests used cloudlog and filled it
ipchandler.close()
ipchandler.connect()
@@ -25,7 +24,7 @@ class TestLogmessaged(unittest.TestCase):
messaging.drain_sock(self.sock)
messaging.drain_sock(self.error_sock)
- def tearDown(self):
+ def teardown_method(self):
del self.sock
del self.error_sock
managed_processes['logmessaged'].stop(block=True)
@@ -55,6 +54,3 @@ class TestLogmessaged(unittest.TestCase):
logsize = sum([os.path.getsize(f) for f in self._get_log_files()])
assert (n*len(msg)) < logsize < (n*(len(msg)+1024))
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/selfdrive/thermald/__init__.py b/system/thermald/__init__.py
similarity index 100%
rename from selfdrive/thermald/__init__.py
rename to system/thermald/__init__.py
diff --git a/selfdrive/thermald/fan_controller.py b/system/thermald/fan_controller.py
similarity index 100%
rename from selfdrive/thermald/fan_controller.py
rename to system/thermald/fan_controller.py
diff --git a/selfdrive/thermald/power_monitoring.py b/system/thermald/power_monitoring.py
similarity index 99%
rename from selfdrive/thermald/power_monitoring.py
rename to system/thermald/power_monitoring.py
index 076c8917a7..bd1513358b 100644
--- a/selfdrive/thermald/power_monitoring.py
+++ b/system/thermald/power_monitoring.py
@@ -5,7 +5,7 @@ from openpilot.common.numpy_fast import interp
from openpilot.common.params import Params
from openpilot.system.hardware import HARDWARE
from openpilot.common.swaglog import cloudlog
-from openpilot.selfdrive.statsd import statlog
+from openpilot.system.statsd import statlog
CAR_VOLTAGE_LOW_PASS_K = 0.011 # LPF gain for 45s tau (dt/tau / (dt/tau + 1))
diff --git a/selfdrive/thermald/tests/__init__.py b/system/thermald/tests/__init__.py
similarity index 100%
rename from selfdrive/thermald/tests/__init__.py
rename to system/thermald/tests/__init__.py
diff --git a/system/thermald/tests/test_fan_controller.py b/system/thermald/tests/test_fan_controller.py
new file mode 100755
index 0000000000..5c858132a2
--- /dev/null
+++ b/system/thermald/tests/test_fan_controller.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+import pytest
+
+from openpilot.system.thermald.fan_controller import TiciFanController
+
+ALL_CONTROLLERS = [TiciFanController]
+
+def patched_controller(mocker, controller_class):
+ mocker.patch("os.system", new=mocker.Mock())
+ return controller_class()
+
+class TestFanController:
+ def wind_up(self, controller, ignition=True):
+ for _ in range(1000):
+ controller.update(100, ignition)
+
+ def wind_down(self, controller, ignition=False):
+ for _ in range(1000):
+ controller.update(10, ignition)
+
+ @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
+ def test_hot_onroad(self, mocker, controller_class):
+ controller = patched_controller(mocker, controller_class)
+ self.wind_up(controller)
+ assert controller.update(100, True) >= 70
+
+ @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
+ def test_offroad_limits(self, mocker, controller_class):
+ controller = patched_controller(mocker, controller_class)
+ self.wind_up(controller)
+ assert controller.update(100, False) <= 30
+
+ @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
+ def test_no_fan_wear(self, mocker, controller_class):
+ controller = patched_controller(mocker, controller_class)
+ self.wind_down(controller)
+ assert controller.update(10, False) == 0
+
+ @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
+ def test_limited(self, mocker, controller_class):
+ controller = patched_controller(mocker, controller_class)
+ self.wind_up(controller, True)
+ assert controller.update(100, True) == 100
+
+ @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS)
+ def test_windup_speed(self, mocker, controller_class):
+ controller = patched_controller(mocker, controller_class)
+ self.wind_down(controller, True)
+ for _ in range(10):
+ controller.update(90, True)
+ assert controller.update(90, True) >= 60
diff --git a/system/thermald/tests/test_power_monitoring.py b/system/thermald/tests/test_power_monitoring.py
new file mode 100755
index 0000000000..0795a29c8f
--- /dev/null
+++ b/system/thermald/tests/test_power_monitoring.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python3
+import pytest
+
+from openpilot.common.params import Params
+from openpilot.system.thermald.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \
+ CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S
+
+# Create fake time
+ssb = 0.
+def mock_time_monotonic():
+ global ssb
+ ssb += 1.
+ return ssb
+
+TEST_DURATION_S = 50
+GOOD_VOLTAGE = 12 * 1e3
+VOLTAGE_BELOW_PAUSE_CHARGING = (VBATT_PAUSE_CHARGING - 1) * 1e3
+
+def pm_patch(mocker, name, value, constant=False):
+ if constant:
+ mocker.patch(f"openpilot.system.thermald.power_monitoring.{name}", value)
+ else:
+ mocker.patch(f"openpilot.system.thermald.power_monitoring.{name}", return_value=value)
+
+
+@pytest.fixture(autouse=True)
+def mock_time(mocker):
+ mocker.patch("time.monotonic", mock_time_monotonic)
+
+
+class TestPowerMonitoring:
+ def setup_method(self):
+ self.params = Params()
+
+ # Test to see that it doesn't do anything when pandaState is None
+ def test_panda_state_present(self):
+ pm = PowerMonitoring()
+ for _ in range(10):
+ pm.calculate(None, None)
+ assert pm.get_power_used() == 0
+ assert pm.get_car_battery_capacity() == (CAR_BATTERY_CAPACITY_uWh / 10)
+
+ # Test to see that it doesn't integrate offroad when ignition is True
+ def test_offroad_ignition(self):
+ pm = PowerMonitoring()
+ for _ in range(10):
+ pm.calculate(GOOD_VOLTAGE, True)
+ assert pm.get_power_used() == 0
+
+ # Test to see that it integrates with discharging battery
+ def test_offroad_integration_discharging(self, mocker):
+ POWER_DRAW = 4
+ pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
+ pm = PowerMonitoring()
+ for _ in range(TEST_DURATION_S + 1):
+ pm.calculate(GOOD_VOLTAGE, False)
+ expected_power_usage = ((TEST_DURATION_S/3600) * POWER_DRAW * 1e6)
+ assert abs(pm.get_power_used() - expected_power_usage) < 10
+
+ # Test to check positive integration of car_battery_capacity
+ def test_car_battery_integration_onroad(self, mocker):
+ POWER_DRAW = 4
+ pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
+ pm = PowerMonitoring()
+ pm.car_battery_capacity_uWh = 0
+ for _ in range(TEST_DURATION_S + 1):
+ pm.calculate(GOOD_VOLTAGE, True)
+ expected_capacity = ((TEST_DURATION_S/3600) * CAR_CHARGING_RATE_W * 1e6)
+ assert abs(pm.get_car_battery_capacity() - expected_capacity) < 10
+
+ # Test to check positive integration upper limit
+ def test_car_battery_integration_upper_limit(self, mocker):
+ POWER_DRAW = 4
+ pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
+ pm = PowerMonitoring()
+ pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh - 1000
+ for _ in range(TEST_DURATION_S + 1):
+ pm.calculate(GOOD_VOLTAGE, True)
+ estimated_capacity = CAR_BATTERY_CAPACITY_uWh + (CAR_CHARGING_RATE_W / 3600 * 1e6)
+ assert abs(pm.get_car_battery_capacity() - estimated_capacity) < 10
+
+ # Test to check negative integration of car_battery_capacity
+ def test_car_battery_integration_offroad(self, mocker):
+ POWER_DRAW = 4
+ pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
+ pm = PowerMonitoring()
+ pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
+ for _ in range(TEST_DURATION_S + 1):
+ pm.calculate(GOOD_VOLTAGE, False)
+ expected_capacity = CAR_BATTERY_CAPACITY_uWh - ((TEST_DURATION_S/3600) * POWER_DRAW * 1e6)
+ assert abs(pm.get_car_battery_capacity() - expected_capacity) < 10
+
+ # Test to check negative integration lower limit
+ def test_car_battery_integration_lower_limit(self, mocker):
+ POWER_DRAW = 4
+ pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
+ pm = PowerMonitoring()
+ pm.car_battery_capacity_uWh = 1000
+ for _ in range(TEST_DURATION_S + 1):
+ pm.calculate(GOOD_VOLTAGE, False)
+ estimated_capacity = 0 - ((1/3600) * POWER_DRAW * 1e6)
+ assert abs(pm.get_car_battery_capacity() - estimated_capacity) < 10
+
+ # Test to check policy of stopping charging after MAX_TIME_OFFROAD_S
+ def test_max_time_offroad(self, mocker):
+ MOCKED_MAX_OFFROAD_TIME = 3600
+ POWER_DRAW = 0 # To stop shutting down for other reasons
+ pm_patch(mocker, "MAX_TIME_OFFROAD_S", MOCKED_MAX_OFFROAD_TIME, constant=True)
+ pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
+ pm = PowerMonitoring()
+ pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
+ start_time = ssb
+ ignition = False
+ while ssb <= start_time + MOCKED_MAX_OFFROAD_TIME:
+ pm.calculate(GOOD_VOLTAGE, ignition)
+ if (ssb - start_time) % 1000 == 0 and ssb < start_time + MOCKED_MAX_OFFROAD_TIME:
+ assert not pm.should_shutdown(ignition, True, start_time, False)
+ assert pm.should_shutdown(ignition, True, start_time, False)
+
+ def test_car_voltage(self, mocker):
+ POWER_DRAW = 0 # To stop shutting down for other reasons
+ TEST_TIME = 350
+ VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S = 50
+ pm_patch(mocker, "VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S", VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S, constant=True)
+ pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
+ pm = PowerMonitoring()
+ pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
+ ignition = False
+ start_time = ssb
+ for i in range(TEST_TIME):
+ pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
+ if i % 10 == 0:
+ assert pm.should_shutdown(ignition, True, start_time, True) == \
+ (pm.car_voltage_mV < VBATT_PAUSE_CHARGING * 1e3 and \
+ (ssb - start_time) > VOLTAGE_SHUTDOWN_MIN_OFFROAD_TIME_S and \
+ (ssb - start_time) > DELAY_SHUTDOWN_TIME_S)
+ assert pm.should_shutdown(ignition, True, start_time, True)
+
+ # Test to check policy of not stopping charging when DisablePowerDown is set
+ def test_disable_power_down(self, mocker):
+ POWER_DRAW = 0 # To stop shutting down for other reasons
+ TEST_TIME = 100
+ self.params.put_bool("DisablePowerDown", True)
+ pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
+ pm = PowerMonitoring()
+ pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
+ ignition = False
+ for i in range(TEST_TIME):
+ pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
+ if i % 10 == 0:
+ assert not pm.should_shutdown(ignition, True, ssb, False)
+ assert not pm.should_shutdown(ignition, True, ssb, False)
+
+ # Test to check policy of not stopping charging when ignition
+ def test_ignition(self, mocker):
+ POWER_DRAW = 0 # To stop shutting down for other reasons
+ TEST_TIME = 100
+ pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
+ pm = PowerMonitoring()
+ pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
+ ignition = True
+ for i in range(TEST_TIME):
+ pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
+ if i % 10 == 0:
+ assert not pm.should_shutdown(ignition, True, ssb, False)
+ assert not pm.should_shutdown(ignition, True, ssb, False)
+
+ # Test to check policy of not stopping charging when harness is not connected
+ def test_harness_connection(self, mocker):
+ POWER_DRAW = 0 # To stop shutting down for other reasons
+ TEST_TIME = 100
+ pm_patch(mocker, "HARDWARE.get_current_power_draw", POWER_DRAW)
+ pm = PowerMonitoring()
+ pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh
+
+ ignition = False
+ for i in range(TEST_TIME):
+ pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
+ if i % 10 == 0:
+ assert not pm.should_shutdown(ignition, False, ssb, False)
+ assert not pm.should_shutdown(ignition, False, ssb, False)
+
+ def test_delay_shutdown_time(self):
+ pm = PowerMonitoring()
+ pm.car_battery_capacity_uWh = 0
+ ignition = False
+ in_car = True
+ offroad_timestamp = ssb
+ started_seen = True
+ pm.calculate(VOLTAGE_BELOW_PAUSE_CHARGING, ignition)
+
+ while ssb < offroad_timestamp + DELAY_SHUTDOWN_TIME_S:
+ assert not pm.should_shutdown(ignition, in_car,
+ offroad_timestamp,
+ started_seen), \
+ f"Should not shutdown before {DELAY_SHUTDOWN_TIME_S} seconds offroad time"
+ assert pm.should_shutdown(ignition, in_car,
+ offroad_timestamp,
+ started_seen), \
+ f"Should shutdown after {DELAY_SHUTDOWN_TIME_S} seconds offroad time"
diff --git a/selfdrive/thermald/thermald.py b/system/thermald/thermald.py
similarity index 98%
rename from selfdrive/thermald/thermald.py
rename to system/thermald/thermald.py
index f5fc949637..90f6494a26 100755
--- a/selfdrive/thermald/thermald.py
+++ b/system/thermald/thermald.py
@@ -19,10 +19,10 @@ from openpilot.common.realtime import DT_TRML
from openpilot.selfdrive.controls.lib.alertmanager import set_offroad_alert
from openpilot.system.hardware import HARDWARE, TICI, AGNOS
from openpilot.system.loggerd.config import get_available_percent
-from openpilot.selfdrive.statsd import statlog
+from openpilot.system.statsd import statlog
from openpilot.common.swaglog import cloudlog
-from openpilot.selfdrive.thermald.power_monitoring import PowerMonitoring
-from openpilot.selfdrive.thermald.fan_controller import TiciFanController
+from openpilot.system.thermald.power_monitoring import PowerMonitoring
+from openpilot.system.thermald.fan_controller import TiciFanController
from openpilot.system.version import terms_version, training_version
ThermalStatus = log.DeviceState.ThermalStatus
diff --git a/selfdrive/tombstoned.py b/system/tombstoned.py
similarity index 99%
rename from selfdrive/tombstoned.py
rename to system/tombstoned.py
index 2c99c7eafe..5bcced2666 100755
--- a/selfdrive/tombstoned.py
+++ b/system/tombstoned.py
@@ -9,7 +9,7 @@ import time
import glob
from typing import NoReturn
-import openpilot.selfdrive.sentry as sentry
+import openpilot.system.sentry as sentry
from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog
from openpilot.system.version import get_build_metadata
diff --git a/system/ubloxd/pigeond.py b/system/ubloxd/pigeond.py
index 21b3a86f97..5711992cfb 100755
--- a/system/ubloxd/pigeond.py
+++ b/system/ubloxd/pigeond.py
@@ -61,7 +61,7 @@ def get_assistnow_messages(token: bytes) -> list[bytes]:
return msgs
-class TTYPigeon():
+class TTYPigeon:
def __init__(self):
self.tty = serial.VTIMESerial(UBLOX_TTY, baudrate=9600, timeout=0)
diff --git a/system/ubloxd/tests/test_pigeond.py b/system/ubloxd/tests/test_pigeond.py
index 742e20bb90..cd3ad9305c 100755
--- a/system/ubloxd/tests/test_pigeond.py
+++ b/system/ubloxd/tests/test_pigeond.py
@@ -1,21 +1,20 @@
#!/usr/bin/env python3
import pytest
import time
-import unittest
import cereal.messaging as messaging
from cereal.services import SERVICE_LIST
from openpilot.common.gpio import gpio_read
from openpilot.selfdrive.test.helpers import with_processes
-from openpilot.selfdrive.manager.process_config import managed_processes
+from openpilot.system.manager.process_config import managed_processes
from openpilot.system.hardware.tici.pins import GPIO
# TODO: test TTFF when we have good A-GNSS
@pytest.mark.tici
-class TestPigeond(unittest.TestCase):
+class TestPigeond:
- def tearDown(self):
+ def teardown_method(self):
managed_processes['pigeond'].stop()
@with_processes(['pigeond'])
@@ -54,7 +53,3 @@ class TestPigeond(unittest.TestCase):
assert gpio_read(GPIO.UBLOX_RST_N) == 0
assert gpio_read(GPIO.GNSS_PWR_EN) == 0
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/system/updated/casync/tar.py b/system/updated/casync/tar.py
index 5c547e73a2..a5a8238bba 100644
--- a/system/updated/casync/tar.py
+++ b/system/updated/casync/tar.py
@@ -1,6 +1,7 @@
import pathlib
import tarfile
-from typing import IO, Callable
+from typing import IO
+from collections.abc import Callable
def include_default(_) -> bool:
diff --git a/system/updated/casync/tests/test_casync.py b/system/updated/casync/tests/test_casync.py
index 34427d5625..29cf78f0f1 100755
--- a/system/updated/casync/tests/test_casync.py
+++ b/system/updated/casync/tests/test_casync.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
+import pytest
import os
import pathlib
-import unittest
import tempfile
import subprocess
@@ -14,9 +14,10 @@ from openpilot.system.updated.casync import tar
LOOPBACK = os.environ.get('LOOPBACK', None)
-class TestCasync(unittest.TestCase):
+@pytest.mark.skip("not used yet")
+class TestCasync:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
cls.tmpdir = tempfile.TemporaryDirectory()
# Build example contents
@@ -43,7 +44,7 @@ class TestCasync(unittest.TestCase):
# Ensure we have chunk reuse
assert len(hashes) > len(set(hashes))
- def setUp(self):
+ def setup_method(self):
# Clear target_lo
if LOOPBACK is not None:
self.target_lo = LOOPBACK
@@ -53,7 +54,7 @@ class TestCasync(unittest.TestCase):
self.target_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names()))
self.seed_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names()))
- def tearDown(self):
+ def teardown_method(self):
for fn in [self.target_fn, self.seed_fn]:
try:
os.unlink(fn)
@@ -67,9 +68,9 @@ class TestCasync(unittest.TestCase):
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as target_f:
- self.assertEqual(target_f.read(), self.contents)
+ assert target_f.read() == self.contents
- self.assertEqual(stats['remote'], len(self.contents))
+ assert stats['remote'] == len(self.contents)
def test_seed(self):
target = casync.parse_caibx(self.manifest_fn)
@@ -83,10 +84,10 @@ class TestCasync(unittest.TestCase):
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as target_f:
- self.assertEqual(target_f.read(), self.contents)
+ assert target_f.read() == self.contents
- self.assertGreater(stats['seed'], 0)
- self.assertLess(stats['remote'], len(self.contents))
+ assert stats['seed'] > 0
+ assert stats['remote'] < len(self.contents)
def test_already_done(self):
"""Test that an already flashed target doesn't download any chunks"""
@@ -101,9 +102,9 @@ class TestCasync(unittest.TestCase):
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as f:
- self.assertEqual(f.read(), self.contents)
+ assert f.read() == self.contents
- self.assertEqual(stats['target'], len(self.contents))
+ assert stats['target'] == len(self.contents)
def test_chunk_reuse(self):
"""Test that chunks that are reused are only downloaded once"""
@@ -119,11 +120,11 @@ class TestCasync(unittest.TestCase):
stats = casync.extract(target, sources, self.target_fn)
with open(self.target_fn, 'rb') as f:
- self.assertEqual(f.read(), self.contents)
+ assert f.read() == self.contents
- self.assertLess(stats['remote'], len(self.contents))
+ assert stats['remote'] < len(self.contents)
- @unittest.skipUnless(LOOPBACK, "requires loopback device")
+ @pytest.mark.skipif(not LOOPBACK, reason="requires loopback device")
def test_lo_simple_extract(self):
target = casync.parse_caibx(self.manifest_fn)
sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
@@ -131,11 +132,11 @@ class TestCasync(unittest.TestCase):
stats = casync.extract(target, sources, self.target_lo)
with open(self.target_lo, 'rb') as target_f:
- self.assertEqual(target_f.read(len(self.contents)), self.contents)
+ assert target_f.read(len(self.contents)) == self.contents
- self.assertEqual(stats['remote'], len(self.contents))
+ assert stats['remote'] == len(self.contents)
- @unittest.skipUnless(LOOPBACK, "requires loopback device")
+ @pytest.mark.skipif(not LOOPBACK, reason="requires loopback device")
def test_lo_chunk_reuse(self):
"""Test that chunks that are reused are only downloaded once"""
target = casync.parse_caibx(self.manifest_fn)
@@ -146,12 +147,13 @@ class TestCasync(unittest.TestCase):
stats = casync.extract(target, sources, self.target_lo)
with open(self.target_lo, 'rb') as f:
- self.assertEqual(f.read(len(self.contents)), self.contents)
+ assert f.read(len(self.contents)) == self.contents
- self.assertLess(stats['remote'], len(self.contents))
+ assert stats['remote'] < len(self.contents)
-class TestCasyncDirectory(unittest.TestCase):
+@pytest.mark.skip("not used yet")
+class TestCasyncDirectory:
"""Tests extracting a directory stored as a casync tar archive"""
NUM_FILES = 16
@@ -174,7 +176,7 @@ class TestCasyncDirectory(unittest.TestCase):
os.symlink(f"file_{i}.txt", os.path.join(directory, f"link_{i}.txt"))
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
cls.tmpdir = tempfile.TemporaryDirectory()
# Create casync files
@@ -190,16 +192,16 @@ class TestCasyncDirectory(unittest.TestCase):
subprocess.check_output(["casync", "make", "--compression=xz", "--store", cls.store_fn, cls.manifest_fn, cls.orig_fn])
@classmethod
- def tearDownClass(cls):
+ def teardown_class(cls):
cls.tmpdir.cleanup()
cls.directory_to_extract.cleanup()
- def setUp(self):
+ def setup_method(self):
self.cache_dir = tempfile.TemporaryDirectory()
self.working_dir = tempfile.TemporaryDirectory()
self.out_dir = tempfile.TemporaryDirectory()
- def tearDown(self):
+ def teardown_method(self):
self.cache_dir.cleanup()
self.working_dir.cleanup()
self.out_dir.cleanup()
@@ -216,32 +218,32 @@ class TestCasyncDirectory(unittest.TestCase):
stats = casync.extract_directory(target, sources, pathlib.Path(self.out_dir.name), tmp_filename)
with open(os.path.join(self.out_dir.name, "file_0.txt"), "rb") as f:
- self.assertEqual(f.read(), self.contents)
+ assert f.read() == self.contents
with open(os.path.join(self.out_dir.name, "link_0.txt"), "rb") as f:
- self.assertEqual(f.read(), self.contents)
- self.assertEqual(os.readlink(os.path.join(self.out_dir.name, "link_0.txt")), "file_0.txt")
+ assert f.read() == self.contents
+ assert os.readlink(os.path.join(self.out_dir.name, "link_0.txt")) == "file_0.txt"
return stats
def test_no_cache(self):
self.setup_cache(self.cache_dir.name, [])
stats = self.run_test()
- self.assertGreater(stats['remote'], 0)
- self.assertEqual(stats['cache'], 0)
+ assert stats['remote'] > 0
+ assert stats['cache'] == 0
def test_full_cache(self):
self.setup_cache(self.cache_dir.name, range(self.NUM_FILES))
stats = self.run_test()
- self.assertEqual(stats['remote'], 0)
- self.assertGreater(stats['cache'], 0)
+ assert stats['remote'] == 0
+ assert stats['cache'] > 0
def test_one_file_cache(self):
self.setup_cache(self.cache_dir.name, range(1))
stats = self.run_test()
- self.assertGreater(stats['remote'], 0)
- self.assertGreater(stats['cache'], 0)
- self.assertLess(stats['cache'], stats['remote'])
+ assert stats['remote'] > 0
+ assert stats['cache'] > 0
+ assert stats['cache'] < stats['remote']
def test_one_file_incorrect_cache(self):
self.setup_cache(self.cache_dir.name, range(self.NUM_FILES))
@@ -249,19 +251,15 @@ class TestCasyncDirectory(unittest.TestCase):
f.write(b"1234")
stats = self.run_test()
- self.assertGreater(stats['remote'], 0)
- self.assertGreater(stats['cache'], 0)
- self.assertGreater(stats['cache'], stats['remote'])
+ assert stats['remote'] > 0
+ assert stats['cache'] > 0
+ assert stats['cache'] > stats['remote']
def test_one_file_missing_cache(self):
self.setup_cache(self.cache_dir.name, range(self.NUM_FILES))
os.unlink(os.path.join(self.cache_dir.name, "file_12.txt"))
stats = self.run_test()
- self.assertGreater(stats['remote'], 0)
- self.assertGreater(stats['cache'], 0)
- self.assertGreater(stats['cache'], stats['remote'])
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert stats['remote'] > 0
+ assert stats['cache'] > 0
+ assert stats['cache'] > stats['remote']
diff --git a/selfdrive/updated/tests/test_base.py b/system/updated/tests/test_base.py
similarity index 82%
rename from selfdrive/updated/tests/test_base.py
rename to system/updated/tests/test_base.py
index b59f03fe77..928d07cbe3 100644
--- a/selfdrive/updated/tests/test_base.py
+++ b/system/updated/tests/test_base.py
@@ -6,13 +6,10 @@ import stat
import subprocess
import tempfile
import time
-import unittest
-from unittest import mock
-
import pytest
from openpilot.common.params import Params
-from openpilot.selfdrive.manager.process import ManagerProcess
+from openpilot.system.manager.process import ManagerProcess
from openpilot.selfdrive.test.helpers import processes_context
@@ -52,13 +49,13 @@ def get_version(path: str) -> str:
@pytest.mark.slow # TODO: can we test overlayfs in GHA?
-class BaseUpdateTest(unittest.TestCase):
+class TestBaseUpdate:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
if "Base" in cls.__name__:
- raise unittest.SkipTest
+ pytest.skip()
- def setUp(self):
+ def setup_method(self):
self.tmpdir = tempfile.mkdtemp()
run(["sudo", "mount", "-t", "tmpfs", "tmpfs", self.tmpdir]) # overlayfs doesn't work inside of docker unless this is a tmpfs
@@ -76,8 +73,6 @@ class BaseUpdateTest(unittest.TestCase):
self.remote_dir = self.mock_update_path / "remote"
self.remote_dir.mkdir()
- mock.patch("openpilot.common.basedir.BASEDIR", self.basedir).start()
-
os.environ["UPDATER_STAGING_ROOT"] = str(self.staging_root)
os.environ["UPDATER_LOCK_FILE"] = str(self.mock_update_path / "safe_staging_overlay.lock")
@@ -86,6 +81,10 @@ class BaseUpdateTest(unittest.TestCase):
"master": ("0.1.3", "1.2", "0.1.3 release notes"),
}
+ @pytest.fixture(autouse=True)
+ def mock_basedir(self, mocker):
+ mocker.patch("openpilot.common.basedir.BASEDIR", self.basedir)
+
def set_target_branch(self, branch):
self.params.put("UpdaterTargetBranch", branch)
@@ -102,8 +101,7 @@ class BaseUpdateTest(unittest.TestCase):
def additional_context(self):
raise NotImplementedError("")
- def tearDown(self):
- mock.patch.stopall()
+ def teardown_method(self):
try:
run(["sudo", "umount", "-l", str(self.staging_root / "merged")])
run(["sudo", "umount", "-l", self.tmpdir])
@@ -125,17 +123,17 @@ class BaseUpdateTest(unittest.TestCase):
time.sleep(1)
def _test_finalized_update(self, branch, version, agnos_version, release_notes):
- self.assertEqual(get_version(str(self.staging_root / "finalized")), version)
- self.assertEqual(get_consistent_flag(str(self.staging_root / "finalized")), True)
- self.assertTrue(os.access(str(self.staging_root / "finalized" / "launch_env.sh"), os.X_OK))
+ assert get_version(str(self.staging_root / "finalized")) == version
+ assert get_consistent_flag(str(self.staging_root / "finalized"))
+ assert os.access(str(self.staging_root / "finalized" / "launch_env.sh"), os.X_OK)
with open(self.staging_root / "finalized" / "test_symlink") as f:
- self.assertIn(version, f.read())
+ assert version in f.read()
-class ParamsBaseUpdateTest(BaseUpdateTest):
+class ParamsBaseUpdateTest(TestBaseUpdate):
def _test_finalized_update(self, branch, version, agnos_version, release_notes):
- self.assertTrue(self.params.get("UpdaterNewDescription", encoding="utf-8").startswith(f"{version} / {branch}"))
- self.assertEqual(self.params.get("UpdaterNewReleaseNotes", encoding="utf-8"), f"{release_notes}
\n")
+ assert self.params.get("UpdaterNewDescription", encoding="utf-8").startswith(f"{version} / {branch}")
+ assert self.params.get("UpdaterNewReleaseNotes", encoding="utf-8") == f"{release_notes}
\n"
super()._test_finalized_update(branch, version, agnos_version, release_notes)
def send_check_for_updates_signal(self, updated: ManagerProcess):
@@ -145,9 +143,9 @@ class ParamsBaseUpdateTest(BaseUpdateTest):
updated.signal(signal.SIGHUP.value)
def _test_params(self, branch, fetch_available, update_available):
- self.assertEqual(self.params.get("UpdaterTargetBranch", encoding="utf-8"), branch)
- self.assertEqual(self.params.get_bool("UpdaterFetchAvailable"), fetch_available)
- self.assertEqual(self.params.get_bool("UpdateAvailable"), update_available)
+ assert self.params.get("UpdaterTargetBranch", encoding="utf-8") == branch
+ assert self.params.get_bool("UpdaterFetchAvailable") == fetch_available
+ assert self.params.get_bool("UpdateAvailable") == update_available
def wait_for_idle(self):
self.wait_for_condition(lambda: self.params.get("UpdaterState", encoding="utf-8") == "idle")
@@ -229,17 +227,16 @@ class ParamsBaseUpdateTest(BaseUpdateTest):
self._test_params("master", False, True)
self._test_finalized_update("master", *self.MOCK_RELEASES["master"])
- def test_agnos_update(self):
+ def test_agnos_update(self, mocker):
# Start on release3, push an update with an agnos change
self.setup_remote_release("release3")
self.setup_basedir_release("release3")
- with self.additional_context(), \
- mock.patch("openpilot.system.hardware.AGNOS", "True"), \
- mock.patch("openpilot.system.hardware.tici.hardware.Tici.get_os_version", "1.2"), \
- mock.patch("openpilot.system.hardware.tici.agnos.get_target_slot_number"), \
- mock.patch("openpilot.system.hardware.tici.agnos.flash_agnos_update"), \
- processes_context(["updated"]) as [updated]:
+ with self.additional_context(), processes_context(["updated"]) as [updated]:
+ mocker.patch("openpilot.system.hardware.AGNOS", "True")
+ mocker.patch("openpilot.system.hardware.tici.hardware.Tici.get_os_version", "1.2")
+ mocker.patch("openpilot.system.hardware.tici.agnos.get_target_slot_number")
+ mocker.patch("openpilot.system.hardware.tici.agnos.flash_agnos_update")
self._test_params("release3", False, False)
self.wait_for_idle()
diff --git a/selfdrive/updated/tests/test_git.py b/system/updated/tests/test_git.py
similarity index 88%
rename from selfdrive/updated/tests/test_git.py
rename to system/updated/tests/test_git.py
index 51ea77fb81..5a5a27000b 100644
--- a/selfdrive/updated/tests/test_git.py
+++ b/system/updated/tests/test_git.py
@@ -1,5 +1,5 @@
import contextlib
-from openpilot.selfdrive.updated.tests.test_base import ParamsBaseUpdateTest, run, update_release
+from openpilot.system.updated.tests.test_base import ParamsBaseUpdateTest, run, update_release
class TestUpdateDGitStrategy(ParamsBaseUpdateTest):
diff --git a/selfdrive/updated/updated.py b/system/updated/updated.py
similarity index 100%
rename from selfdrive/updated/updated.py
rename to system/updated/updated.py
diff --git a/system/version.py b/system/version.py
index f5b8cd49e8..d258fe0492 100755
--- a/system/version.py
+++ b/system/version.py
@@ -27,7 +27,7 @@ def get_version(path: str = BASEDIR) -> str:
def get_release_notes(path: str = BASEDIR) -> str:
- with open(os.path.join(path, "RELEASES.md"), "r") as f:
+ with open(os.path.join(path, "RELEASES.md")) as f:
return f.read().split('\n\n', 1)[0]
diff --git a/system/webrtc/tests/test_stream_session.py b/system/webrtc/tests/test_stream_session.py
index 2173c3806b..d8defab13f 100755
--- a/system/webrtc/tests/test_stream_session.py
+++ b/system/webrtc/tests/test_stream_session.py
@@ -1,7 +1,5 @@
#!/usr/bin/env python3
import asyncio
-import unittest
-from unittest.mock import Mock, MagicMock, patch
import json
# for aiortc and its dependencies
import warnings
@@ -20,15 +18,15 @@ from openpilot.system.webrtc.device.audio import AudioInputStreamTrack
from openpilot.common.realtime import DT_DMON
-class TestStreamSession(unittest.TestCase):
- def setUp(self):
+class TestStreamSession:
+ def setup_method(self):
self.loop = asyncio.new_event_loop()
- def tearDown(self):
+ def teardown_method(self):
self.loop.stop()
self.loop.close()
- def test_outgoing_proxy(self):
+ def test_outgoing_proxy(self, mocker):
test_msg = log.Event.new_message()
test_msg.logMonoTime = 123
test_msg.valid = True
@@ -36,27 +34,27 @@ class TestStreamSession(unittest.TestCase):
expected_dict = {"type": "customReservedRawData0", "logMonoTime": 123, "valid": True, "data": "test"}
expected_json = json.dumps(expected_dict).encode()
- channel = Mock(spec=RTCDataChannel)
+ channel = mocker.Mock(spec=RTCDataChannel)
mocked_submaster = messaging.SubMaster(["customReservedRawData0"])
def mocked_update(t):
mocked_submaster.update_msgs(0, [test_msg])
- with patch.object(messaging.SubMaster, "update", side_effect=mocked_update):
- proxy = CerealOutgoingMessageProxy(mocked_submaster)
- proxy.add_channel(channel)
+ mocker.patch.object(messaging.SubMaster, "update", side_effect=mocked_update)
+ proxy = CerealOutgoingMessageProxy(mocked_submaster)
+ proxy.add_channel(channel)
- proxy.update()
+ proxy.update()
- channel.send.assert_called_once_with(expected_json)
+ channel.send.assert_called_once_with(expected_json)
- def test_incoming_proxy(self):
+ def test_incoming_proxy(self, mocker):
tested_msgs = [
{"type": "customReservedRawData0", "data": "test"}, # primitive
{"type": "can", "data": [{"address": 0, "busTime": 0, "dat": "", "src": 0}]}, # list
{"type": "testJoystick", "data": {"axes": [0, 0], "buttons": [False]}}, # dict
]
- mocked_pubmaster = MagicMock(spec=messaging.PubMaster)
+ mocked_pubmaster = mocker.MagicMock(spec=messaging.PubMaster)
proxy = CerealIncomingMessageProxy(mocked_pubmaster)
@@ -65,44 +63,40 @@ class TestStreamSession(unittest.TestCase):
mocked_pubmaster.send.assert_called_once()
mt, md = mocked_pubmaster.send.call_args.args
- self.assertEqual(mt, msg["type"])
- self.assertIsInstance(md, capnp._DynamicStructBuilder)
- self.assertTrue(hasattr(md, msg["type"]))
+ assert mt == msg["type"]
+ assert isinstance(md, capnp._DynamicStructBuilder)
+ assert hasattr(md, msg["type"])
mocked_pubmaster.reset_mock()
- def test_livestream_track(self):
+ def test_livestream_track(self, mocker):
fake_msg = messaging.new_message("livestreamDriverEncodeData")
config = {"receive.return_value": fake_msg.to_bytes()}
- with patch("cereal.messaging.SubSocket", spec=True, **config):
- track = LiveStreamVideoStreamTrack("driver")
+ mocker.patch("cereal.messaging.SubSocket", spec=True, **config)
+ track = LiveStreamVideoStreamTrack("driver")
- self.assertTrue(track.id.startswith("driver"))
- self.assertEqual(track.codec_preference(), "H264")
+ assert track.id.startswith("driver")
+ assert track.codec_preference() == "H264"
- for i in range(5):
- packet = self.loop.run_until_complete(track.recv())
- self.assertEqual(packet.time_base, VIDEO_TIME_BASE)
- self.assertEqual(packet.pts, int(i * DT_DMON * VIDEO_CLOCK_RATE))
- self.assertEqual(packet.size, 0)
+ for i in range(5):
+ packet = self.loop.run_until_complete(track.recv())
+ assert packet.time_base == VIDEO_TIME_BASE
+ assert packet.pts == int(i * DT_DMON * VIDEO_CLOCK_RATE)
+ assert packet.size == 0
- def test_input_audio_track(self):
+ def test_input_audio_track(self, mocker):
packet_time, rate = 0.02, 16000
sample_count = int(packet_time * rate)
- mocked_stream = MagicMock(spec=pyaudio.Stream)
+ mocked_stream = mocker.MagicMock(spec=pyaudio.Stream)
mocked_stream.read.return_value = b"\x00" * 2 * sample_count
config = {"open.side_effect": lambda *args, **kwargs: mocked_stream}
- with patch("pyaudio.PyAudio", spec=True, **config):
- track = AudioInputStreamTrack(audio_format=pyaudio.paInt16, packet_time=packet_time, rate=rate)
+ mocker.patch("pyaudio.PyAudio", spec=True, **config)
+ track = AudioInputStreamTrack(audio_format=pyaudio.paInt16, packet_time=packet_time, rate=rate)
- for i in range(5):
- frame = self.loop.run_until_complete(track.recv())
- self.assertEqual(frame.rate, rate)
- self.assertEqual(frame.samples, sample_count)
- self.assertEqual(frame.pts, i * sample_count)
-
-
-if __name__ == "__main__":
- unittest.main()
+ for i in range(5):
+ frame = self.loop.run_until_complete(track.recv())
+ assert frame.rate == rate
+ assert frame.samples == sample_count
+ assert frame.pts == i * sample_count
diff --git a/system/webrtc/tests/test_webrtcd.py b/system/webrtc/tests/test_webrtcd.py
index e5742dba07..b0bc7b1966 100755
--- a/system/webrtc/tests/test_webrtcd.py
+++ b/system/webrtc/tests/test_webrtcd.py
@@ -1,8 +1,7 @@
#!/usr/bin/env python
+import pytest
import asyncio
import json
-import unittest
-from unittest.mock import MagicMock, AsyncMock
# for aiortc and its dependencies
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
@@ -20,19 +19,20 @@ from parameterized import parameterized_class
(["testJoystick"], []),
([], []),
])
-class TestWebrtcdProc(unittest.IsolatedAsyncioTestCase):
+@pytest.mark.asyncio
+class TestWebrtcdProc:
async def assertCompletesWithTimeout(self, awaitable, timeout=1):
try:
async with asyncio.timeout(timeout):
await awaitable
except TimeoutError:
- self.fail("Timeout while waiting for awaitable to complete")
+ pytest.fail("Timeout while waiting for awaitable to complete")
- async def test_webrtcd(self):
- mock_request = MagicMock()
+ async def test_webrtcd(self, mocker):
+ mock_request = mocker.MagicMock()
async def connect(offer):
body = {'sdp': offer.sdp, 'cameras': offer.video, 'bridge_services_in': self.in_services, 'bridge_services_out': self.out_services}
- mock_request.json.side_effect = AsyncMock(return_value=body)
+ mock_request.json.side_effect = mocker.AsyncMock(return_value=body)
response = await get_stream(mock_request)
response_json = json.loads(response.text)
return aiortc.RTCSessionDescription(**response_json)
@@ -48,9 +48,9 @@ class TestWebrtcdProc(unittest.IsolatedAsyncioTestCase):
await self.assertCompletesWithTimeout(stream.start())
await self.assertCompletesWithTimeout(stream.wait_for_connection())
- self.assertTrue(stream.has_incoming_video_track("road"))
- self.assertTrue(stream.has_incoming_audio_track())
- self.assertEqual(stream.has_messaging_channel(), len(self.in_services) > 0 or len(self.out_services) > 0)
+ assert stream.has_incoming_video_track("road")
+ assert stream.has_incoming_audio_track()
+ assert stream.has_messaging_channel() == (len(self.in_services) > 0 or len(self.out_services) > 0)
video_track, audio_track = stream.get_incoming_video_track("road"), stream.get_incoming_audio_track()
await self.assertCompletesWithTimeout(video_track.recv())
@@ -59,10 +59,6 @@ class TestWebrtcdProc(unittest.IsolatedAsyncioTestCase):
await self.assertCompletesWithTimeout(stream.stop())
# cleanup, very implementation specific, test may break if it changes
- self.assertTrue(mock_request.app["streams"].__setitem__.called, "Implementation changed, please update this test")
+ assert mock_request.app["streams"].__setitem__.called, "Implementation changed, please update this test"
_, session = mock_request.app["streams"].__setitem__.call_args.args
await self.assertCompletesWithTimeout(session.post_run_cleanup())
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/system/webrtc/webrtcd.py b/system/webrtc/webrtcd.py
index 51c86aacc6..76c3cd8470 100755
--- a/system/webrtc/webrtcd.py
+++ b/system/webrtc/webrtcd.py
@@ -97,8 +97,8 @@ class CerealProxyRunner:
except InvalidStateError:
self.logger.warning("Cereal outgoing proxy invalid state (connection closed)")
break
- except Exception as ex:
- self.logger.error("Cereal outgoing proxy failure: %s", ex)
+ except Exception:
+ self.logger.exception("Cereal outgoing proxy failure")
await asyncio.sleep(0.01)
@@ -175,8 +175,8 @@ class StreamSession:
assert self.incoming_bridge is not None
try:
self.incoming_bridge.send(message)
- except Exception as ex:
- self.logger.error("Cereal incoming proxy failure: %s", ex)
+ except Exception:
+ self.logger.exception("Cereal incoming proxy failure")
async def run(self):
try:
@@ -200,8 +200,8 @@ class StreamSession:
await self.post_run_cleanup()
self.logger.info("Stream session (%s) ended", self.identifier)
- except Exception as ex:
- self.logger.error("Stream session failure: %s", ex)
+ except Exception:
+ self.logger.exception("Stream session failure")
async def post_run_cleanup(self):
await self.stream.stop()
diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h
index 3d63ee49bd..cfd423b368 100644
--- a/tools/cabana/streams/abstractstream.h
+++ b/tools/cabana/streams/abstractstream.h
@@ -64,6 +64,7 @@ public:
AbstractStream(QObject *parent);
virtual ~AbstractStream() {}
virtual void start() = 0;
+ virtual void stop() {}
virtual bool liveStreaming() const { return true; }
virtual void seekTo(double ts) {}
virtual QString routeName() const = 0;
@@ -128,11 +129,15 @@ private:
};
class AbstractOpenStreamWidget : public QWidget {
+ Q_OBJECT
public:
AbstractOpenStreamWidget(AbstractStream **stream, QWidget *parent = nullptr) : stream(stream), QWidget(parent) {}
virtual bool open() = 0;
virtual QString title() = 0;
+signals:
+ void enableOpenButton(bool);
+
protected:
AbstractStream **stream = nullptr;
};
diff --git a/tools/cabana/streams/livestream.cc b/tools/cabana/streams/livestream.cc
index 4e1f6d77d9..d9d96e23b0 100644
--- a/tools/cabana/streams/livestream.cc
+++ b/tools/cabana/streams/livestream.cc
@@ -42,6 +42,10 @@ LiveStream::LiveStream(QObject *parent) : AbstractStream(parent) {
QObject::connect(stream_thread, &QThread::finished, stream_thread, &QThread::deleteLater);
}
+LiveStream::~LiveStream() {
+ stop();
+}
+
void LiveStream::startUpdateTimer() {
update_timer.stop();
update_timer.start(1000.0 / settings.fps, this);
@@ -55,11 +59,14 @@ void LiveStream::start() {
begin_date_time = QDateTime::currentDateTime();
}
-LiveStream::~LiveStream() {
+void LiveStream::stop() {
+ if (!stream_thread) return;
+
update_timer.stop();
stream_thread->requestInterruption();
stream_thread->quit();
stream_thread->wait();
+ stream_thread = nullptr;
}
// called in streamThread
diff --git a/tools/cabana/streams/livestream.h b/tools/cabana/streams/livestream.h
index 7a0f8cd834..9ca7e8d745 100644
--- a/tools/cabana/streams/livestream.h
+++ b/tools/cabana/streams/livestream.h
@@ -14,6 +14,7 @@ public:
LiveStream(QObject *parent);
virtual ~LiveStream();
void start() override;
+ void stop() override;
inline QDateTime beginDateTime() const { return begin_date_time; }
inline double routeStartTime() const override { return begin_event_ts / 1e9; }
void setSpeed(float speed) override { speed_ = speed; }
diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc
index 308391a10c..030783a899 100644
--- a/tools/cabana/streams/pandastream.cc
+++ b/tools/cabana/streams/pandastream.cc
@@ -6,12 +6,17 @@
#include
#include
#include
+#include
-PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) {}
+PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) {
+ if (!connect()) {
+ throw std::runtime_error("Failed to connect to panda");
+ }
+}
bool PandaStream::connect() {
try {
- qDebug() << "Connecting to panda with serial" << config.serial;
+ qDebug() << "Connecting to panda " << config.serial;
panda.reset(new Panda(config.serial.toStdString()));
config.bus_config.resize(3);
qDebug() << "Connected";
@@ -80,6 +85,13 @@ AbstractOpenStreamWidget *PandaStream::widget(AbstractStream **stream) {
OpenPandaWidget::OpenPandaWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) {
form_layout = new QFormLayout(this);
+ if (can && dynamic_cast(can) != nullptr) {
+ form_layout->addWidget(new QLabel(tr("Already connected to %1.").arg(can->routeName())));
+ form_layout->addWidget(new QLabel("Close the current connection via [File menu -> Close Stream] before connecting to another Panda."));
+ QTimer::singleShot(0, [this]() { emit enableOpenButton(false); });
+ return;
+ }
+
QHBoxLayout *serial_layout = new QHBoxLayout();
serial_layout->addWidget(serial_edit = new QComboBox());
@@ -116,6 +128,7 @@ void OpenPandaWidget::buildConfigForm() {
Panda panda(serial.toStdString());
has_fd = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2);
} catch (const std::exception& e) {
+ qDebug() << "failed to open panda" << serial;
has_panda = false;
}
}
@@ -170,13 +183,11 @@ void OpenPandaWidget::buildConfigForm() {
}
bool OpenPandaWidget::open() {
- if (!config.serial.isEmpty()) {
- auto panda_stream = std::make_unique(qApp, config);
- if (panda_stream->connect()) {
- *stream = panda_stream.release();
- return true;
- }
+ try {
+ *stream = new PandaStream(qApp, config);
+ return true;
+ } catch (std::exception &e) {
+ QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what()));
+ return false;
}
- QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda"));
- return false;
}
diff --git a/tools/cabana/streams/pandastream.h b/tools/cabana/streams/pandastream.h
index ce0adfc9f8..c50407be1a 100644
--- a/tools/cabana/streams/pandastream.h
+++ b/tools/cabana/streams/pandastream.h
@@ -21,13 +21,14 @@ class PandaStream : public LiveStream {
Q_OBJECT
public:
PandaStream(QObject *parent, PandaStreamConfig config_ = {});
- bool connect();
+ ~PandaStream() { stop(); }
static AbstractOpenStreamWidget *widget(AbstractStream **stream);
inline QString routeName() const override {
- return QString("Live Streaming From Panda %1").arg(config.serial);
+ return QString("Panda: %1").arg(config.serial);
}
protected:
+ bool connect();
void streamThread() override;
std::unique_ptr panda;
diff --git a/tools/cabana/streams/socketcanstream.h b/tools/cabana/streams/socketcanstream.h
index e0fb826acb..952f1aaa5c 100644
--- a/tools/cabana/streams/socketcanstream.h
+++ b/tools/cabana/streams/socketcanstream.h
@@ -17,6 +17,7 @@ class SocketCanStream : public LiveStream {
Q_OBJECT
public:
SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {});
+ ~SocketCanStream() { stop(); }
static AbstractOpenStreamWidget *widget(AbstractStream **stream);
static bool available();
diff --git a/tools/cabana/streamselector.cc b/tools/cabana/streamselector.cc
index d12f1a5df7..209b577c9a 100644
--- a/tools/cabana/streamselector.cc
+++ b/tools/cabana/streamselector.cc
@@ -1,6 +1,5 @@
#include "tools/cabana/streamselector.h"
-#include
#include
#include
#include
@@ -31,7 +30,7 @@ StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDial
line->setFrameStyle(QFrame::HLine | QFrame::Sunken);
layout->addWidget(line);
- auto btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel);
+ btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel);
layout->addWidget(btn_box);
addStreamWidget(ReplayStream::widget(stream));
@@ -60,4 +59,6 @@ StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDial
void StreamSelector::addStreamWidget(AbstractOpenStreamWidget *w) {
tab->addTab(w, w->title());
+ auto open_btn = btn_box->button(QDialogButtonBox::Open);
+ QObject::connect(w, &AbstractOpenStreamWidget::enableOpenButton, open_btn, &QPushButton::setEnabled);
}
diff --git a/tools/cabana/streamselector.h b/tools/cabana/streamselector.h
index 19b438d55a..a702fc76ad 100644
--- a/tools/cabana/streamselector.h
+++ b/tools/cabana/streamselector.h
@@ -1,5 +1,6 @@
#pragma once
+#include
#include
#include
#include
@@ -17,4 +18,5 @@ public:
private:
QLineEdit *dbc_file;
QTabWidget *tab;
+ QDialogButtonBox *btn_box;
};
diff --git a/tools/car_porting/test_car_model.py b/tools/car_porting/test_car_model.py
index 5f8294fd3c..cf0be1a80a 100755
--- a/tools/car_porting/test_car_model.py
+++ b/tools/car_porting/test_car_model.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
import argparse
import sys
-import unittest
+import unittest # noqa: TID251
from openpilot.selfdrive.car.tests.routes import CarTestRoute
from openpilot.selfdrive.car.tests.test_models import TestCarModel
diff --git a/tools/install_ubuntu_dependencies.sh b/tools/install_ubuntu_dependencies.sh
index 75d19141b6..1f8ef24eaf 100755
--- a/tools/install_ubuntu_dependencies.sh
+++ b/tools/install_ubuntu_dependencies.sh
@@ -12,24 +12,16 @@ if [[ ! $(id -u) -eq 0 ]]; then
SUDO="sudo"
fi
-# Install packages present in all supported versions of Ubuntu
+# Install common packages
function install_ubuntu_common_requirements() {
$SUDO apt-get update
$SUDO apt-get install -y --no-install-recommends \
- autoconf \
- build-essential \
ca-certificates \
- casync \
clang \
- cmake \
- make \
cppcheck \
- libtool \
+ build-essential \
gcc-arm-none-eabi \
- bzip2 \
liblzma-dev \
- libarchive-dev \
- libbz2-dev \
capnproto \
libcapnp-dev \
curl \
@@ -42,43 +34,53 @@ function install_ubuntu_common_requirements() {
libavdevice-dev \
libavutil-dev \
libavfilter-dev \
+ libbz2-dev \
libeigen3-dev \
libffi-dev \
libglew-dev \
libgles2-mesa-dev \
libglfw3-dev \
libglib2.0-0 \
+ libqt5charts5-dev \
libncurses5-dev \
- libncursesw5-dev \
- libomp-dev \
- libopencv-dev \
- libpng16-16 \
- libportaudio2 \
libssl-dev \
- libsqlite3-dev \
libusb-1.0-0-dev \
libzmq3-dev \
+ libsqlite3-dev \
libsystemd-dev \
locales \
opencl-headers \
ocl-icd-libopencl1 \
ocl-icd-opencl-dev \
- clinfo \
portaudio19-dev \
- qml-module-qtquick2 \
qtmultimedia5-dev \
qtlocation5-dev \
qtpositioning5-dev \
qttools5-dev-tools \
- libqt5sql5-sqlite \
libqt5svg5-dev \
- libqt5charts5-dev \
libqt5serialbus5-dev \
libqt5x11extras5-dev \
- libqt5opengl5-dev \
+ libqt5opengl5-dev
+}
+
+# Install extra packages
+function install_extra_packages() {
+ echo "Installing extra packages..."
+ $SUDO apt-get install -y --no-install-recommends \
+ casync \
+ cmake \
+ make \
+ clinfo \
+ libqt5sql5-sqlite \
libreadline-dev \
libdw1 \
- valgrind
+ autoconf \
+ libtool \
+ bzip2 \
+ libarchive-dev \
+ libncursesw5-dev \
+ libportaudio2 \
+ locales
}
# Install Ubuntu 24.04 LTS packages
@@ -127,7 +129,19 @@ if [ -f "/etc/os-release" ]; then
install_ubuntu_lts_latest_requirements
fi
esac
+
+ # Install extra packages
+ if [[ -z "$INSTALL_EXTRA_PACKAGES" ]]; then
+ read -p "Base setup done. Do you want to install extra development packages? [Y/n]: " -n 1 -r
+ echo ""
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ INSTALL_EXTRA_PACKAGES="yes"
+ fi
+ fi
+ if [[ "$INSTALL_EXTRA_PACKAGES" == "yes" ]]; then
+ install_extra_packages
+ fi
else
- echo "No /etc/os-release in the system"
+ echo "No /etc/os-release in the system. Make sure you're running on Ubuntu, or similar."
exit 1
fi
diff --git a/tools/lib/api.py b/tools/lib/api.py
index 6ff9242f29..92a75d2a3b 100644
--- a/tools/lib/api.py
+++ b/tools/lib/api.py
@@ -2,7 +2,7 @@ import os
import requests
API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com')
-class CommaApi():
+class CommaApi:
def __init__(self, token=None):
self.session = requests.Session()
self.session.headers['User-agent'] = 'OpenpilotTools'
diff --git a/tools/lib/filereader.py b/tools/lib/filereader.py
index e9b8b4b2ce..3bfdc75feb 100644
--- a/tools/lib/filereader.py
+++ b/tools/lib/filereader.py
@@ -10,10 +10,11 @@ DATA_ENDPOINT = os.getenv("DATA_ENDPOINT", "http://data-raw.comma.internal/")
def internal_source_available():
try:
hostname = urlparse(DATA_ENDPOINT).hostname
- if hostname:
- socket.gethostbyname(hostname)
- return True
- except socket.gaierror:
+ port = urlparse(DATA_ENDPOINT).port or 80
+ with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
+ s.connect((hostname, port))
+ return True
+ except (socket.gaierror, ConnectionRefusedError):
pass
return False
diff --git a/tools/lib/tests/test_caching.py b/tools/lib/tests/test_caching.py
index 5d3dfeba42..32f55df5d1 100755
--- a/tools/lib/tests/test_caching.py
+++ b/tools/lib/tests/test_caching.py
@@ -1,13 +1,11 @@
#!/usr/bin/env python3
-from functools import partial
import http.server
import os
import shutil
import socket
-import unittest
+import pytest
-from parameterized import parameterized
-from openpilot.selfdrive.test.helpers import with_http_server
+from openpilot.selfdrive.test.helpers import http_server_context
from openpilot.system.hardware.hw import Paths
from openpilot.tools.lib.url_file import URLFile
@@ -31,22 +29,23 @@ class CachingTestRequestHandler(http.server.BaseHTTPRequestHandler):
self.end_headers()
-with_caching_server = partial(with_http_server, handler=CachingTestRequestHandler)
+@pytest.fixture
+def host():
+ with http_server_context(handler=CachingTestRequestHandler) as (host, port):
+ yield f"http://{host}:{port}"
+class TestFileDownload:
-class TestFileDownload(unittest.TestCase):
-
- @with_caching_server
def test_pipeline_defaults(self, host):
# TODO: parameterize the defaults so we don't rely on hard-coded values in xx
- self.assertEqual(URLFile.pool_manager().pools._maxsize, 10) # PoolManager num_pools param
+ assert URLFile.pool_manager().pools._maxsize == 10# PoolManager num_pools param
pool_manager_defaults = {
"maxsize": 100,
"socket_options": [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),],
}
for k, v in pool_manager_defaults.items():
- self.assertEqual(URLFile.pool_manager().connection_pool_kw.get(k), v)
+ assert URLFile.pool_manager().connection_pool_kw.get(k) == v
retry_defaults = {
"total": 5,
@@ -54,7 +53,7 @@ class TestFileDownload(unittest.TestCase):
"status_forcelist": [409, 429, 503, 504],
}
for k, v in retry_defaults.items():
- self.assertEqual(getattr(URLFile.pool_manager().connection_pool_kw["retries"], k), v)
+ assert getattr(URLFile.pool_manager().connection_pool_kw["retries"], k) == v
# ensure caching off by default and cache dir doesn't get created
os.environ.pop("FILEREADER_CACHE", None)
@@ -62,7 +61,7 @@ class TestFileDownload(unittest.TestCase):
shutil.rmtree(Paths.download_cache_root())
URLFile(f"{host}/test.txt").get_length()
URLFile(f"{host}/test.txt").read()
- self.assertEqual(os.path.exists(Paths.download_cache_root()), False)
+ assert not os.path.exists(Paths.download_cache_root())
def compare_loads(self, url, start=0, length=None):
"""Compares range between cached and non cached version"""
@@ -72,21 +71,21 @@ class TestFileDownload(unittest.TestCase):
file_cached.seek(start)
file_downloaded.seek(start)
- self.assertEqual(file_cached.get_length(), file_downloaded.get_length())
- self.assertLessEqual(length + start if length is not None else 0, file_downloaded.get_length())
+ assert file_cached.get_length() == file_downloaded.get_length()
+ assert length + start if length is not None else 0 <= file_downloaded.get_length()
response_cached = file_cached.read(ll=length)
response_downloaded = file_downloaded.read(ll=length)
- self.assertEqual(response_cached, response_downloaded)
+ assert response_cached == response_downloaded
# Now test with cache in place
file_cached = URLFile(url, cache=True)
file_cached.seek(start)
response_cached = file_cached.read(ll=length)
- self.assertEqual(file_cached.get_length(), file_downloaded.get_length())
- self.assertEqual(response_cached, response_downloaded)
+ assert file_cached.get_length() == file_downloaded.get_length()
+ assert response_cached == response_downloaded
def test_small_file(self):
# Make sure we don't force cache
@@ -117,22 +116,16 @@ class TestFileDownload(unittest.TestCase):
self.compare_loads(large_file_url, length - 100, 100)
self.compare_loads(large_file_url)
- @parameterized.expand([(True, ), (False, )])
- @with_caching_server
- def test_recover_from_missing_file(self, cache_enabled, host):
+ @pytest.mark.parametrize("cache_enabled", [True, False])
+ def test_recover_from_missing_file(self, host, cache_enabled):
os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0"
file_url = f"{host}/test.png"
CachingTestRequestHandler.FILE_EXISTS = False
length = URLFile(file_url).get_length()
- self.assertEqual(length, -1)
+ assert length == -1
CachingTestRequestHandler.FILE_EXISTS = True
length = URLFile(file_url).get_length()
- self.assertEqual(length, 4)
-
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert length == 4
diff --git a/tools/lib/tests/test_comma_car_segments.py b/tools/lib/tests/test_comma_car_segments.py
index 91bab94343..b9a4def75f 100644
--- a/tools/lib/tests/test_comma_car_segments.py
+++ b/tools/lib/tests/test_comma_car_segments.py
@@ -1,5 +1,4 @@
import pytest
-import unittest
import requests
from openpilot.selfdrive.car.fingerprints import MIGRATION
from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database, get_url
@@ -8,7 +7,7 @@ from openpilot.tools.lib.route import SegmentRange
@pytest.mark.skip(reason="huggingface is flaky, run this test manually to check for issues")
-class TestCommaCarSegments(unittest.TestCase):
+class TestCommaCarSegments:
def test_database(self):
database = get_comma_car_segments_database()
@@ -28,12 +27,8 @@ class TestCommaCarSegments(unittest.TestCase):
url = get_url(sr.route_name, sr.slice)
resp = requests.get(url)
- self.assertEqual(resp.status_code, 200)
+ assert resp.status_code == 200
lr = LogReader(url)
CP = lr.first("carParams")
- self.assertEqual(MIGRATION.get(CP.carFingerprint, CP.carFingerprint), fp)
-
-
-if __name__ == "__main__":
- unittest.main()
+ assert MIGRATION.get(CP.carFingerprint, CP.carFingerprint) == fp
diff --git a/tools/lib/tests/test_logreader.py b/tools/lib/tests/test_logreader.py
index fc72202b26..58d22a07ef 100755
--- a/tools/lib/tests/test_logreader.py
+++ b/tools/lib/tests/test_logreader.py
@@ -5,12 +5,10 @@ import io
import shutil
import tempfile
import os
-import unittest
import pytest
import requests
from parameterized import parameterized
-from unittest import mock
from cereal import log as capnp_log
from openpilot.tools.lib.logreader import LogIterable, LogReader, comma_api_source, parse_indirect, ReadMode, InternalUnavailableException
@@ -28,24 +26,22 @@ def noop(segment: LogIterable):
@contextlib.contextmanager
-def setup_source_scenario(is_internal=False):
- with (
- mock.patch("openpilot.tools.lib.logreader.internal_source") as internal_source_mock,
- mock.patch("openpilot.tools.lib.logreader.openpilotci_source") as openpilotci_source_mock,
- mock.patch("openpilot.tools.lib.logreader.comma_api_source") as comma_api_source_mock,
- ):
- if is_internal:
- internal_source_mock.return_value = [QLOG_FILE]
- else:
- internal_source_mock.side_effect = InternalUnavailableException
+def setup_source_scenario(mocker, is_internal=False):
+ internal_source_mock = mocker.patch("openpilot.tools.lib.logreader.internal_source")
+ openpilotci_source_mock = mocker.patch("openpilot.tools.lib.logreader.openpilotci_source")
+ comma_api_source_mock = mocker.patch("openpilot.tools.lib.logreader.comma_api_source")
+ if is_internal:
+ internal_source_mock.return_value = [QLOG_FILE]
+ else:
+ internal_source_mock.side_effect = InternalUnavailableException
- openpilotci_source_mock.return_value = [None]
- comma_api_source_mock.return_value = [QLOG_FILE]
+ openpilotci_source_mock.return_value = [None]
+ comma_api_source_mock.return_value = [QLOG_FILE]
- yield
+ yield
-class TestLogReader(unittest.TestCase):
+class TestLogReader:
@parameterized.expand([
(f"{TEST_ROUTE}", ALL_SEGS),
(f"{TEST_ROUTE.replace('/', '|')}", ALL_SEGS),
@@ -74,7 +70,7 @@ class TestLogReader(unittest.TestCase):
def test_indirect_parsing(self, identifier, expected):
parsed, _, _ = parse_indirect(identifier)
sr = SegmentRange(parsed)
- self.assertListEqual(list(sr.seg_idxs), expected, identifier)
+ assert list(sr.seg_idxs) == expected, identifier
@parameterized.expand([
(f"{TEST_ROUTE}", f"{TEST_ROUTE}"),
@@ -86,11 +82,11 @@ class TestLogReader(unittest.TestCase):
])
def test_canonical_name(self, identifier, expected):
sr = SegmentRange(identifier)
- self.assertEqual(str(sr), expected)
+ assert str(sr) == expected
- @parameterized.expand([(True,), (False,)])
- @mock.patch("openpilot.tools.lib.logreader.file_exists")
- def test_direct_parsing(self, cache_enabled, file_exists_mock):
+ @pytest.mark.parametrize("cache_enabled", [True, False])
+ def test_direct_parsing(self, mocker, cache_enabled):
+ file_exists_mock = mocker.patch("openpilot.tools.lib.logreader.file_exists")
os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0"
qlog = tempfile.NamedTemporaryFile(mode='wb', delete=False)
@@ -100,13 +96,13 @@ class TestLogReader(unittest.TestCase):
for f in [QLOG_FILE, qlog.name]:
l = len(list(LogReader(f)))
- self.assertGreater(l, 100)
+ assert l > 100
- with self.assertRaises(URLFileException) if not cache_enabled else self.assertRaises(AssertionError):
+ with pytest.raises(URLFileException) if not cache_enabled else pytest.raises(AssertionError):
l = len(list(LogReader(QLOG_FILE.replace("/3/", "/200/"))))
# file_exists should not be called for direct files
- self.assertEqual(file_exists_mock.call_count, 0)
+ assert file_exists_mock.call_count == 0
@parameterized.expand([
(f"{TEST_ROUTE}///",),
@@ -121,110 +117,110 @@ class TestLogReader(unittest.TestCase):
(f"{TEST_ROUTE}--3a",),
])
def test_bad_ranges(self, segment_range):
- with self.assertRaises(AssertionError):
+ with pytest.raises(AssertionError):
_ = SegmentRange(segment_range).seg_idxs
- @parameterized.expand([
+ @pytest.mark.parametrize("segment_range, api_call", [
(f"{TEST_ROUTE}/0", False),
(f"{TEST_ROUTE}/:2", False),
(f"{TEST_ROUTE}/0:", True),
(f"{TEST_ROUTE}/-1", True),
(f"{TEST_ROUTE}", True),
])
- def test_slicing_api_call(self, segment_range, api_call):
- with mock.patch("openpilot.tools.lib.route.get_max_seg_number_cached") as max_seg_mock:
- max_seg_mock.return_value = NUM_SEGS
- _ = SegmentRange(segment_range).seg_idxs
- self.assertEqual(api_call, max_seg_mock.called)
+ def test_slicing_api_call(self, mocker, segment_range, api_call):
+ max_seg_mock = mocker.patch("openpilot.tools.lib.route.get_max_seg_number_cached")
+ max_seg_mock.return_value = NUM_SEGS
+ _ = SegmentRange(segment_range).seg_idxs
+ assert api_call == max_seg_mock.called
@pytest.mark.slow
def test_modes(self):
qlog_len = len(list(LogReader(f"{TEST_ROUTE}/0", ReadMode.QLOG)))
rlog_len = len(list(LogReader(f"{TEST_ROUTE}/0", ReadMode.RLOG)))
- self.assertLess(qlog_len * 6, rlog_len)
+ assert qlog_len * 6 < rlog_len
@pytest.mark.slow
def test_modes_from_name(self):
qlog_len = len(list(LogReader(f"{TEST_ROUTE}/0/q")))
rlog_len = len(list(LogReader(f"{TEST_ROUTE}/0/r")))
- self.assertLess(qlog_len * 6, rlog_len)
+ assert qlog_len * 6 < rlog_len
@pytest.mark.slow
def test_list(self):
qlog_len = len(list(LogReader(f"{TEST_ROUTE}/0/q")))
qlog_len_2 = len(list(LogReader([f"{TEST_ROUTE}/0/q", f"{TEST_ROUTE}/0/q"])))
- self.assertEqual(qlog_len * 2, qlog_len_2)
+ assert qlog_len * 2 == qlog_len_2
@pytest.mark.slow
- @mock.patch("openpilot.tools.lib.logreader._LogFileReader")
- def test_multiple_iterations(self, init_mock):
+ def test_multiple_iterations(self, mocker):
+ init_mock = mocker.patch("openpilot.tools.lib.logreader._LogFileReader")
lr = LogReader(f"{TEST_ROUTE}/0/q")
qlog_len1 = len(list(lr))
qlog_len2 = len(list(lr))
# ensure we don't create multiple instances of _LogFileReader, which means downloading the files twice
- self.assertEqual(init_mock.call_count, 1)
+ assert init_mock.call_count == 1
- self.assertEqual(qlog_len1, qlog_len2)
+ assert qlog_len1 == qlog_len2
@pytest.mark.slow
def test_helpers(self):
lr = LogReader(f"{TEST_ROUTE}/0/q")
- self.assertEqual(lr.first("carParams").carFingerprint, "SUBARU OUTBACK 6TH GEN")
- self.assertTrue(0 < len(list(lr.filter("carParams"))) < len(list(lr)))
+ assert lr.first("carParams").carFingerprint == "SUBARU OUTBACK 6TH GEN"
+ assert 0 < len(list(lr.filter("carParams"))) < len(list(lr))
@parameterized.expand([(True,), (False,)])
@pytest.mark.slow
def test_run_across_segments(self, cache_enabled):
os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0"
lr = LogReader(f"{TEST_ROUTE}/0:4")
- self.assertEqual(len(lr.run_across_segments(4, noop)), len(list(lr)))
+ assert len(lr.run_across_segments(4, noop)) == len(list(lr))
@pytest.mark.slow
- def test_auto_mode(self):
+ def test_auto_mode(self, subtests, mocker):
lr = LogReader(f"{TEST_ROUTE}/0/q")
qlog_len = len(list(lr))
- with mock.patch("openpilot.tools.lib.route.Route.log_paths") as log_paths_mock:
- log_paths_mock.return_value = [None] * NUM_SEGS
- # Should fall back to qlogs since rlogs are not available
+ log_paths_mock = mocker.patch("openpilot.tools.lib.route.Route.log_paths")
+ log_paths_mock.return_value = [None] * NUM_SEGS
+ # Should fall back to qlogs since rlogs are not available
- with self.subTest("interactive_yes"):
- with mock.patch("sys.stdin", new=io.StringIO("y\n")):
- lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, default_source=comma_api_source)
- log_len = len(list(lr))
- self.assertEqual(qlog_len, log_len)
+ with subtests.test("interactive_yes"):
+ mocker.patch("sys.stdin", new=io.StringIO("y\n"))
+ lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, default_source=comma_api_source)
+ log_len = len(list(lr))
+ assert qlog_len == log_len
- with self.subTest("interactive_no"):
- with mock.patch("sys.stdin", new=io.StringIO("n\n")):
- with self.assertRaises(AssertionError):
- lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, default_source=comma_api_source)
+ with subtests.test("interactive_no"):
+ mocker.patch("sys.stdin", new=io.StringIO("n\n"))
+ with pytest.raises(AssertionError):
+ lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, default_source=comma_api_source)
- with self.subTest("non_interactive"):
- lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO, default_source=comma_api_source)
- log_len = len(list(lr))
- self.assertEqual(qlog_len, log_len)
+ with subtests.test("non_interactive"):
+ lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO, default_source=comma_api_source)
+ log_len = len(list(lr))
+ assert qlog_len == log_len
- @parameterized.expand([(True,), (False,)])
+ @pytest.mark.parametrize("is_internal", [True, False])
@pytest.mark.slow
- def test_auto_source_scenarios(self, is_internal):
+ def test_auto_source_scenarios(self, mocker, is_internal):
lr = LogReader(QLOG_FILE)
qlog_len = len(list(lr))
- with setup_source_scenario(is_internal=is_internal):
+ with setup_source_scenario(mocker, is_internal=is_internal):
lr = LogReader(f"{TEST_ROUTE}/0/q")
log_len = len(list(lr))
- self.assertEqual(qlog_len, log_len)
+ assert qlog_len == log_len
@pytest.mark.slow
def test_sort_by_time(self):
msgs = list(LogReader(f"{TEST_ROUTE}/0/q"))
- self.assertNotEqual(msgs, sorted(msgs, key=lambda m: m.logMonoTime))
+ assert msgs != sorted(msgs, key=lambda m: m.logMonoTime)
msgs = list(LogReader(f"{TEST_ROUTE}/0/q", sort_by_time=True))
- self.assertEqual(msgs, sorted(msgs, key=lambda m: m.logMonoTime))
+ assert msgs == sorted(msgs, key=lambda m: m.logMonoTime)
def test_only_union_types(self):
with tempfile.NamedTemporaryFile() as qlog:
@@ -234,7 +230,7 @@ class TestLogReader(unittest.TestCase):
f.write(b"".join(capnp_log.Event.new_message().to_bytes() for _ in range(num_msgs)))
msgs = list(LogReader(qlog.name))
- self.assertEqual(len(msgs), num_msgs)
+ assert len(msgs) == num_msgs
[m.which() for m in msgs]
# append non-union Event message
@@ -246,15 +242,11 @@ class TestLogReader(unittest.TestCase):
# ensure new message is added, but is not a union type
msgs = list(LogReader(qlog.name))
- self.assertEqual(len(msgs), num_msgs + 1)
- with self.assertRaises(capnp.KjException):
+ assert len(msgs) == num_msgs + 1
+ with pytest.raises(capnp.KjException):
[m.which() for m in msgs]
# should not be added when only_union_types=True
msgs = list(LogReader(qlog.name, only_union_types=True))
- self.assertEqual(len(msgs), num_msgs)
+ assert len(msgs) == num_msgs
[m.which() for m in msgs]
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tools/lib/tests/test_readers.py b/tools/lib/tests/test_readers.py
index 1f24ae5c8e..f92554872f 100755
--- a/tools/lib/tests/test_readers.py
+++ b/tools/lib/tests/test_readers.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-import unittest
+import pytest
import requests
import tempfile
@@ -9,16 +9,16 @@ from openpilot.tools.lib.framereader import FrameReader
from openpilot.tools.lib.logreader import LogReader
-class TestReaders(unittest.TestCase):
- @unittest.skip("skip for bandwidth reasons")
+class TestReaders:
+ @pytest.mark.skip("skip for bandwidth reasons")
def test_logreader(self):
def _check_data(lr):
hist = defaultdict(int)
for l in lr:
hist[l.which()] += 1
- self.assertEqual(hist['carControl'], 6000)
- self.assertEqual(hist['logMessage'], 6857)
+ assert hist['carControl'] == 6000
+ assert hist['logMessage'] == 6857
with tempfile.NamedTemporaryFile(suffix=".bz2") as fp:
r = requests.get("https://github.com/commaai/comma2k19/blob/master/Example_1/b0c9d2329ad1606b%7C2018-08-02--08-34-47/40/raw_log.bz2?raw=true", timeout=10)
@@ -31,15 +31,15 @@ class TestReaders(unittest.TestCase):
lr_url = LogReader("https://github.com/commaai/comma2k19/blob/master/Example_1/b0c9d2329ad1606b%7C2018-08-02--08-34-47/40/raw_log.bz2?raw=true")
_check_data(lr_url)
- @unittest.skip("skip for bandwidth reasons")
+ @pytest.mark.skip("skip for bandwidth reasons")
def test_framereader(self):
def _check_data(f):
- self.assertEqual(f.frame_count, 1200)
- self.assertEqual(f.w, 1164)
- self.assertEqual(f.h, 874)
+ assert f.frame_count == 1200
+ assert f.w == 1164
+ assert f.h == 874
frame_first_30 = f.get(0, 30)
- self.assertEqual(len(frame_first_30), 30)
+ assert len(frame_first_30) == 30
print(frame_first_30[15])
@@ -62,6 +62,3 @@ class TestReaders(unittest.TestCase):
fr_url = FrameReader("https://github.com/commaai/comma2k19/blob/master/Example_1/b0c9d2329ad1606b%7C2018-08-02--08-34-47/40/video.hevc?raw=true")
_check_data(fr_url)
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tools/lib/tests/test_route_library.py b/tools/lib/tests/test_route_library.py
index 7977f17be2..8f75fa19c0 100755
--- a/tools/lib/tests/test_route_library.py
+++ b/tools/lib/tests/test_route_library.py
@@ -1,10 +1,9 @@
#!/usr/bin/env python
-import unittest
from collections import namedtuple
from openpilot.tools.lib.route import SegmentName
-class TestRouteLibrary(unittest.TestCase):
+class TestRouteLibrary:
def test_segment_name_formats(self):
Case = namedtuple('Case', ['input', 'expected_route', 'expected_segment_num', 'expected_data_dir'])
@@ -21,12 +20,9 @@ class TestRouteLibrary(unittest.TestCase):
s = SegmentName(route_or_segment_name, allow_route_name=True)
- self.assertEqual(str(s.route_name), case.expected_route)
- self.assertEqual(s.segment_num, case.expected_segment_num)
- self.assertEqual(s.data_dir, case.expected_data_dir)
+ assert str(s.route_name) == case.expected_route
+ assert s.segment_num == case.expected_segment_num
+ assert s.data_dir == case.expected_data_dir
for case in cases:
_validate(case)
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh
index 9ec2097ea4..d26ec3cfe2 100755
--- a/tools/mac_setup.sh
+++ b/tools/mac_setup.sh
@@ -63,7 +63,6 @@ brew "pyenv"
brew "pyenv-virtualenv"
brew "qt@5"
brew "zeromq"
-brew "gcc@13"
cask "gcc-arm-embedded"
brew "portaudio"
EOS
diff --git a/tools/plotjuggler/README.md b/tools/plotjuggler/README.md
index dcf2bbbac7..dc301efcb7 100644
--- a/tools/plotjuggler/README.md
+++ b/tools/plotjuggler/README.md
@@ -58,7 +58,7 @@ If streaming to PlotJuggler from a replay on your PC, simply run: `./juggle.py -
For a quick demo, go through the installation step and run this command:
-`./juggle.py --demo --qlog --layout=layouts/demo.xml`
+`./juggle.py --demo --layout=layouts/demo.xml`
## Layouts
diff --git a/tools/plotjuggler/test_plotjuggler.py b/tools/plotjuggler/test_plotjuggler.py
index 17287fb803..b9a55297e5 100755
--- a/tools/plotjuggler/test_plotjuggler.py
+++ b/tools/plotjuggler/test_plotjuggler.py
@@ -4,7 +4,6 @@ import glob
import signal
import subprocess
import time
-import unittest
from openpilot.common.basedir import BASEDIR
from openpilot.common.timeout import Timeout
@@ -12,7 +11,7 @@ from openpilot.tools.plotjuggler.juggle import DEMO_ROUTE, install
PJ_DIR = os.path.join(BASEDIR, "tools/plotjuggler")
-class TestPlotJuggler(unittest.TestCase):
+class TestPlotJuggler:
def test_demo(self):
install()
@@ -27,14 +26,14 @@ class TestPlotJuggler(unittest.TestCase):
output += p.stderr.readline().decode("utf-8")
# ensure plotjuggler didn't crash after exiting the plugin
- time.sleep(15)
- self.assertEqual(p.poll(), None)
+ time.sleep(2)
+ assert p.poll() is None
os.killpg(os.getpgid(p.pid), signal.SIGTERM)
- self.assertNotIn("Raw file read failed", output)
+ assert "Raw file read failed" not in output
# TODO: also test that layouts successfully load
- def test_layouts(self):
+ def test_layouts(self, subtests):
bad_strings = (
# if a previously loaded file is defined,
# PJ will throw a warning when loading the layout
@@ -43,12 +42,8 @@ class TestPlotJuggler(unittest.TestCase):
)
for fn in glob.glob(os.path.join(PJ_DIR, "layouts/*")):
name = os.path.basename(fn)
- with self.subTest(layout=name):
+ with subtests.test(layout=name):
with open(fn) as f:
layout = f.read()
violations = [s for s in bad_strings if s in layout]
assert len(violations) == 0, f"These should be stripped out of the layout: {str(violations)}"
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tools/rerun/run.py b/tools/rerun/run.py
new file mode 100755
index 0000000000..8a002f68ba
--- /dev/null
+++ b/tools/rerun/run.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+import sys
+import argparse
+import multiprocessing
+import rerun as rr
+import rerun.blueprint as rrb
+from functools import partial
+
+from openpilot.tools.lib.logreader import LogReader
+from cereal.services import SERVICE_LIST
+
+
+NUM_CPUS = multiprocessing.cpu_count()
+DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19"
+
+def log_msg(msg, parent_key=''):
+ stack = [(msg, parent_key)]
+ while stack:
+ current_msg, current_parent_key = stack.pop()
+ if isinstance(current_msg, list):
+ for index, item in enumerate(current_msg):
+ new_key = f"{current_parent_key}/{index}"
+ if isinstance(item, (int, float)):
+ rr.log(str(new_key), rr.Scalar(item))
+ elif isinstance(item, dict):
+ stack.append((item, new_key))
+ elif isinstance(current_msg, dict):
+ for key, value in current_msg.items():
+ new_key = f"{current_parent_key}/{key}"
+ if isinstance(value, (int, float)):
+ rr.log(str(new_key), rr.Scalar(value))
+ elif isinstance(value, dict):
+ stack.append((value, new_key))
+ elif isinstance(value, list):
+ for index, item in enumerate(value):
+ if isinstance(item, (int, float)):
+ rr.log(f"{new_key}/{index}", rr.Scalar(item))
+ else:
+ pass # Not a plottable value
+
+def createBlueprint():
+ timeSeriesViews = []
+ for topic in sorted(SERVICE_LIST.keys()):
+ timeSeriesViews.append(rrb.TimeSeriesView(name=topic, origin=f"/{topic}/", visible=False))
+ rr.log(topic, rr.SeriesLine(name=topic), timeless=True)
+ blueprint = rrb.Blueprint(rrb.Grid(rrb.Vertical(*timeSeriesViews,rrb.SelectionPanel(expanded=False),rrb.TimePanel(expanded=False)),
+ rrb.Spatial2DView(name="thumbnail", origin="/thumbnail")))
+ return blueprint
+
+def log_thumbnail(thumbnailMsg):
+ bytesImgData = thumbnailMsg.get('thumbnail')
+ rr.log("/thumbnail", rr.ImageEncoded(contents=bytesImgData))
+
+def process(blueprint, lr):
+ ret = []
+ rr.init("rerun_test", spawn=True, default_blueprint=blueprint)
+ for msg in lr:
+ ret.append(msg)
+ rr.set_time_nanos("TIMELINE", msg.logMonoTime)
+ if msg.which() != "thumbnail":
+ log_msg(msg.to_dict()[msg.which()], msg.which())
+ else:
+ log_thumbnail(msg.to_dict()[msg.which()])
+ return ret
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description="A helper to run rerun on openpilot routes",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+ parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one")
+ parser.add_argument("route_or_segment_name", nargs='?', help="The route or segment name to plot")
+
+ if len(sys.argv) == 1:
+ parser.print_help()
+ sys.exit()
+
+ args = parser.parse_args()
+
+ route_or_segment_name = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip()
+ blueprint = createBlueprint()
+ print("Getting route log paths")
+ lr = LogReader(route_or_segment_name)
+ lr.run_across_segments(NUM_CPUS, partial(process, blueprint))
diff --git a/tools/sim/bridge/common.py b/tools/sim/bridge/common.py
index 46d09c7b9d..64407143f7 100644
--- a/tools/sim/bridge/common.py
+++ b/tools/sim/bridge/common.py
@@ -2,6 +2,8 @@ import signal
import threading
import functools
+from collections import namedtuple
+from enum import Enum
from multiprocessing import Process, Queue, Value
from abc import ABC, abstractmethod
@@ -14,6 +16,16 @@ from openpilot.tools.sim.lib.common import SimulatorState, World
from openpilot.tools.sim.lib.simulated_car import SimulatedCar
from openpilot.tools.sim.lib.simulated_sensors import SimulatedSensors
+QueueMessage = namedtuple("QueueMessage", ["type", "info"], defaults=[None])
+
+class QueueMessageType(Enum):
+ START_STATUS = 0
+ CONTROL_COMMAND = 1
+ TERMINATION_INFO = 2
+ CLOSE_STATUS = 3
+
+def control_cmd_gen(cmd: str):
+ return QueueMessage(QueueMessageType.CONTROL_COMMAND, cmd)
def rk_loop(function, hz, exit_event: threading.Event):
rk = Ratekeeper(hz, None)
@@ -48,6 +60,8 @@ class SimulatorBridge(ABC):
self.past_startup_engaged = False
self.startup_button_prev = True
+ self.test_run = False
+
def _on_shutdown(self, signal, frame):
self.shutdown()
@@ -80,11 +94,11 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
""")
@abstractmethod
- def spawn_world(self) -> World:
+ def spawn_world(self, q: Queue) -> World:
pass
def _run(self, q: Queue):
- self.world = self.spawn_world()
+ self.world = self.spawn_world(q)
self.simulated_car = SimulatedCar()
self.simulated_sensors = SimulatedSensors(self.dual_camera)
@@ -114,33 +128,34 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
# Read manual controls
if not q.empty():
message = q.get()
- m = message.split('_')
- if m[0] == "steer":
- steer_manual = float(m[1])
- elif m[0] == "throttle":
- throttle_manual = float(m[1])
- elif m[0] == "brake":
- brake_manual = float(m[1])
- elif m[0] == "cruise":
- if m[1] == "down":
- self.simulator_state.cruise_button = CruiseButtons.DECEL_SET
- elif m[1] == "up":
- self.simulator_state.cruise_button = CruiseButtons.RES_ACCEL
- elif m[1] == "cancel":
- self.simulator_state.cruise_button = CruiseButtons.CANCEL
- elif m[1] == "main":
- self.simulator_state.cruise_button = CruiseButtons.MAIN
- elif m[0] == "blinker":
- if m[1] == "left":
- self.simulator_state.left_blinker = True
- elif m[1] == "right":
- self.simulator_state.right_blinker = True
- elif m[0] == "ignition":
- self.simulator_state.ignition = not self.simulator_state.ignition
- elif m[0] == "reset":
- self.world.reset()
- elif m[0] == "quit":
- break
+ if message.type == QueueMessageType.CONTROL_COMMAND:
+ m = message.info.split('_')
+ if m[0] == "steer":
+ steer_manual = float(m[1])
+ elif m[0] == "throttle":
+ throttle_manual = float(m[1])
+ elif m[0] == "brake":
+ brake_manual = float(m[1])
+ elif m[0] == "cruise":
+ if m[1] == "down":
+ self.simulator_state.cruise_button = CruiseButtons.DECEL_SET
+ elif m[1] == "up":
+ self.simulator_state.cruise_button = CruiseButtons.RES_ACCEL
+ elif m[1] == "cancel":
+ self.simulator_state.cruise_button = CruiseButtons.CANCEL
+ elif m[1] == "main":
+ self.simulator_state.cruise_button = CruiseButtons.MAIN
+ elif m[0] == "blinker":
+ if m[1] == "left":
+ self.simulator_state.left_blinker = True
+ elif m[1] == "right":
+ self.simulator_state.right_blinker = True
+ elif m[0] == "ignition":
+ self.simulator_state.ignition = not self.simulator_state.ignition
+ elif m[0] == "reset":
+ self.world.reset()
+ elif m[0] == "quit":
+ break
self.simulator_state.user_brake = brake_manual
self.simulator_state.user_gas = throttle_manual
@@ -180,7 +195,8 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga
self.world.tick()
self.world.read_cameras()
- if self.rk.frame % 25 == 0:
+ # don't print during test, so no print/IO Block between OP and metadrive processes
+ if not self.test_run and self.rk.frame % 25 == 0:
self.print_status()
self.started.value = True
diff --git a/tools/sim/bridge/metadrive/metadrive_bridge.py b/tools/sim/bridge/metadrive/metadrive_bridge.py
index 5c91238c55..74dc1210b9 100644
--- a/tools/sim/bridge/metadrive/metadrive_bridge.py
+++ b/tools/sim/bridge/metadrive/metadrive_bridge.py
@@ -1,3 +1,4 @@
+import math
from multiprocessing import Queue
from metadrive.component.sensors.base_camera import _cuda_enable
@@ -27,20 +28,21 @@ def curve_block(length, angle=45, direction=0):
}
def create_map(track_size=60):
+ curve_len = track_size * 2
return dict(
type=MapGenerateMethod.PG_MAP_FILE,
lane_num=2,
- lane_width=3.5,
+ lane_width=4.5,
config=[
None,
straight_block(track_size),
- curve_block(track_size*2, 90),
+ curve_block(curve_len, 90),
straight_block(track_size),
- curve_block(track_size*2, 90),
+ curve_block(curve_len, 90),
straight_block(track_size),
- curve_block(track_size*2, 90),
+ curve_block(curve_len, 90),
straight_block(track_size),
- curve_block(track_size*2, 90),
+ curve_block(curve_len, 90),
]
)
@@ -48,12 +50,14 @@ def create_map(track_size=60):
class MetaDriveBridge(SimulatorBridge):
TICKS_PER_FRAME = 5
- def __init__(self, dual_camera, high_quality):
- self.should_render = False
-
+ def __init__(self, dual_camera, high_quality, test_duration=math.inf, test_run=False):
super().__init__(dual_camera, high_quality)
- def spawn_world(self):
+ self.should_render = False
+ self.test_run = test_run
+ self.test_duration = test_duration if self.test_run else math.inf
+
+ def spawn_world(self, queue: Queue):
sensors = {
"rgb_road": (RGBCameraRoad, W, H, )
}
@@ -83,4 +87,4 @@ class MetaDriveBridge(SimulatorBridge):
preload_models=False
)
- return MetaDriveWorld(Queue(), config, self.dual_camera)
+ return MetaDriveWorld(queue, config, self.test_duration, self.test_run, self.dual_camera)
diff --git a/tools/sim/bridge/metadrive/metadrive_process.py b/tools/sim/bridge/metadrive/metadrive_process.py
index 79eefcd545..38c13a1434 100644
--- a/tools/sim/bridge/metadrive/metadrive_process.py
+++ b/tools/sim/bridge/metadrive/metadrive_process.py
@@ -1,4 +1,5 @@
import math
+import time
import numpy as np
from collections import namedtuple
@@ -49,7 +50,7 @@ def apply_metadrive_patches(arrive_dest_done=True):
def metadrive_process(dual_camera: bool, config: dict, camera_array, wide_camera_array, image_lock,
controls_recv: Connection, simulation_state_send: Connection, vehicle_state_send: Connection,
- exit_event):
+ exit_event, op_engaged, test_duration, test_run):
arrive_dest_done = config.pop("arrive_dest_done", True)
apply_metadrive_patches(arrive_dest_done)
@@ -60,9 +61,15 @@ def metadrive_process(dual_camera: bool, config: dict, camera_array, wide_camera
env = MetaDriveEnv(config)
+ def get_current_lane_info(vehicle):
+ _, lane_info, on_lane = vehicle.navigation._get_current_lane(vehicle)
+ lane_idx = lane_info[2] if lane_info is not None else None
+ return lane_idx, on_lane
+
def reset():
env.reset()
env.vehicle.config["max_speed_km_h"] = 1000
+ lane_idx_prev, _ = get_current_lane_info(env.vehicle)
simulation_state = metadrive_simulation_state(
running=True,
@@ -71,14 +78,17 @@ def metadrive_process(dual_camera: bool, config: dict, camera_array, wide_camera
)
simulation_state_send.send(simulation_state)
- reset()
+ return lane_idx_prev
+
+ lane_idx_prev = reset()
+ start_time = None
def get_cam_as_rgb(cam):
cam = env.engine.sensors[cam]
cam.get_cam().reparentTo(env.vehicle.origin)
cam.get_cam().setPos(C3_POSITION)
cam.get_cam().setHpr(C3_HPR)
- img = cam.perceive(clip=False)
+ img = cam.perceive(to_float=False)
if type(img) != np.ndarray:
img = img.get() # convert cupy array to numpy
return img
@@ -95,7 +105,6 @@ def metadrive_process(dual_camera: bool, config: dict, camera_array, wide_camera
bearing=float(math.degrees(env.vehicle.heading_theta)),
steering_angle=env.vehicle.steering * env.vehicle.MAX_STEERING
)
-
vehicle_state_send.send(vehicle_state)
if controls_recv.poll(0):
@@ -108,13 +117,28 @@ def metadrive_process(dual_camera: bool, config: dict, camera_array, wide_camera
vc = [steer_metadrive, gas]
if should_reset:
- reset()
+ lane_idx_prev = reset()
+ start_time = None
+
+ is_engaged = op_engaged.is_set()
+ if is_engaged and start_time is None:
+ start_time = time.monotonic()
if rk.frame % 5 == 0:
- obs, _, terminated, _, info = env.step(vc)
+ _, _, terminated, _, _ = env.step(vc)
+ timeout = True if start_time is not None and time.monotonic() - start_time >= test_duration else False
+ lane_idx_curr, on_lane = get_current_lane_info(env.vehicle)
+ out_of_lane = lane_idx_curr != lane_idx_prev or not on_lane
+ lane_idx_prev = lane_idx_curr
+
+ if terminated or ((out_of_lane or timeout) and test_run):
+ if terminated:
+ done_result = env.done_function("default_agent")
+ elif out_of_lane:
+ done_result = (True, {"out_of_lane" : True})
+ elif timeout:
+ done_result = (True, {"timeout" : True})
- if terminated:
- done_result = env.done_function("default_agent")
simulation_state = metadrive_simulation_state(
running=False,
done=done_result[0],
diff --git a/tools/sim/bridge/metadrive/metadrive_world.py b/tools/sim/bridge/metadrive/metadrive_world.py
index 3570205069..5bb4555dc1 100644
--- a/tools/sim/bridge/metadrive/metadrive_world.py
+++ b/tools/sim/bridge/metadrive/metadrive_world.py
@@ -5,6 +5,8 @@ import numpy as np
import time
from multiprocessing import Pipe, Array
+
+from openpilot.tools.sim.bridge.common import QueueMessage, QueueMessageType
from openpilot.tools.sim.bridge.metadrive.metadrive_process import (metadrive_process, metadrive_simulation_state,
metadrive_vehicle_state)
from openpilot.tools.sim.lib.common import SimulatorState, World
@@ -12,7 +14,7 @@ from openpilot.tools.sim.lib.camerad import W, H
class MetaDriveWorld(World):
- def __init__(self, status_q, config, dual_camera = False):
+ def __init__(self, status_q, config, test_duration, test_run, dual_camera=False):
super().__init__(dual_camera)
self.status_q = status_q
self.camera_array = Array(ctypes.c_uint8, W*H*3)
@@ -27,22 +29,29 @@ class MetaDriveWorld(World):
self.vehicle_state_send, self.vehicle_state_recv = Pipe()
self.exit_event = multiprocessing.Event()
+ self.op_engaged = multiprocessing.Event()
+
+ self.test_run = test_run
+
+ self.first_engage = None
+ self.last_check_timestamp = 0
+ self.distance_moved = 0
self.metadrive_process = multiprocessing.Process(name="metadrive process", target=
functools.partial(metadrive_process, dual_camera, config,
self.camera_array, self.wide_camera_array, self.image_lock,
self.controls_recv, self.simulation_state_send,
- self.vehicle_state_send, self.exit_event))
+ self.vehicle_state_send, self.exit_event, self.op_engaged, test_duration, self.test_run))
self.metadrive_process.start()
- self.status_q.put({"status": "starting"})
+ self.status_q.put(QueueMessage(QueueMessageType.START_STATUS, "starting"))
print("----------------------------------------------------------")
print("---- Spawning Metadrive world, this might take awhile ----")
print("----------------------------------------------------------")
- self.vehicle_state_recv.recv() # wait for a state message to ensure metadrive is launched
- self.status_q.put({"status": "started"})
+ self.vehicle_last_pos = self.vehicle_state_recv.recv().position # wait for a state message to ensure metadrive is launched
+ self.status_q.put(QueueMessage(QueueMessageType.START_STATUS, "started"))
self.steer_ratio = 15
self.vc = [0.0,0.0]
@@ -68,22 +77,46 @@ class MetaDriveWorld(World):
while self.simulation_state_recv.poll(0):
md_state: metadrive_simulation_state = self.simulation_state_recv.recv()
if md_state.done:
- self.status_q.put({
- "status": "terminating",
- "reason": "done",
- "done_info": md_state.done_info
- })
+ self.status_q.put(QueueMessage(QueueMessageType.TERMINATION_INFO, md_state.done_info))
self.exit_event.set()
def read_sensors(self, state: SimulatorState):
while self.vehicle_state_recv.poll(0):
md_vehicle: metadrive_vehicle_state = self.vehicle_state_recv.recv()
+ curr_pos = md_vehicle.position
+
state.velocity = md_vehicle.velocity
state.bearing = md_vehicle.bearing
state.steering_angle = md_vehicle.steering_angle
- state.gps.from_xy(md_vehicle.position)
+ state.gps.from_xy(curr_pos)
state.valid = True
+ is_engaged = state.is_engaged
+ if is_engaged and self.first_engage is None:
+ self.first_engage = time.monotonic()
+ self.op_engaged.set()
+
+ # check moving 5 seconds after engaged, doesn't move right away
+ after_engaged_check = is_engaged and time.monotonic() - self.first_engage >= 5 and self.test_run
+
+ x_dist = abs(curr_pos[0] - self.vehicle_last_pos[0])
+ y_dist = abs(curr_pos[1] - self.vehicle_last_pos[1])
+ dist_threshold = 1
+ if x_dist >= dist_threshold or y_dist >= dist_threshold: # position not the same during staying still, > threshold is considered moving
+ self.distance_moved += x_dist + y_dist
+
+ time_check_threshold = 30
+ current_time = time.monotonic()
+ since_last_check = current_time - self.last_check_timestamp
+ if since_last_check >= time_check_threshold:
+ if after_engaged_check and self.distance_moved == 0:
+ self.status_q.put(QueueMessage(QueueMessageType.TERMINATION_INFO, {"vehicle_not_moving" : True}))
+ self.exit_event.set()
+
+ self.last_check_timestamp = current_time
+ self.distance_moved = 0
+ self.vehicle_last_pos = curr_pos
+
def read_cameras(self):
pass
@@ -94,9 +127,6 @@ class MetaDriveWorld(World):
self.should_reset = True
def close(self, reason: str):
- self.status_q.put({
- "status": "terminating",
- "reason": reason,
- })
+ self.status_q.put(QueueMessage(QueueMessageType.CLOSE_STATUS, reason))
self.exit_event.set()
self.metadrive_process.join()
diff --git a/tools/sim/build_container.sh b/tools/sim/build_container.sh
deleted file mode 100755
index 451277d590..0000000000
--- a/tools/sim/build_container.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-
-DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-cd $DIR/../../
-
-docker pull ghcr.io/commaai/openpilot-base:latest
-docker build \
- --cache-from ghcr.io/commaai/openpilot-sim:latest \
- -t ghcr.io/commaai/openpilot-sim:latest \
- -f tools/sim/Dockerfile.sim .
diff --git a/tools/sim/launch_openpilot.sh b/tools/sim/launch_openpilot.sh
index 86d9607bb0..1b4ac4ef84 100755
--- a/tools/sim/launch_openpilot.sh
+++ b/tools/sim/launch_openpilot.sh
@@ -18,4 +18,4 @@ SCRIPT_DIR=$(dirname "$0")
OPENPILOT_DIR=$SCRIPT_DIR/../../
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
-cd $OPENPILOT_DIR/selfdrive/manager && exec ./manager.py
+cd $OPENPILOT_DIR/system/manager && exec ./manager.py
diff --git a/tools/sim/lib/keyboard_ctrl.py b/tools/sim/lib/keyboard_ctrl.py
index ea255d9ce4..0a17f0ee85 100644
--- a/tools/sim/lib/keyboard_ctrl.py
+++ b/tools/sim/lib/keyboard_ctrl.py
@@ -2,10 +2,13 @@ import sys
import termios
import time
+from multiprocessing import Queue
from termios import (BRKINT, CS8, CSIZE, ECHO, ICANON, ICRNL, IEXTEN, INPCK,
ISTRIP, IXON, PARENB, VMIN, VTIME)
from typing import NoReturn
+from openpilot.tools.sim.bridge.common import QueueMessage, control_cmd_gen
+
# Indexes for termios list.
IFLAG = 0
OFLAG = 1
@@ -52,35 +55,35 @@ def getch() -> str:
def print_keyboard_help():
print(f"Keyboard Commands:\n{KEYBOARD_HELP}")
-def keyboard_poll_thread(q: 'Queue[str]'):
+def keyboard_poll_thread(q: 'Queue[QueueMessage]'):
print_keyboard_help()
while True:
c = getch()
if c == '1':
- q.put("cruise_up")
+ q.put(control_cmd_gen("cruise_up"))
elif c == '2':
- q.put("cruise_down")
+ q.put(control_cmd_gen("cruise_down"))
elif c == '3':
- q.put("cruise_cancel")
+ q.put(control_cmd_gen("cruise_cancel"))
elif c == 'w':
- q.put("throttle_%f" % 1.0)
+ q.put(control_cmd_gen(f"throttle_{1.0}"))
elif c == 'a':
- q.put("steer_%f" % -0.15)
+ q.put(control_cmd_gen(f"steer_{-0.15}"))
elif c == 's':
- q.put("brake_%f" % 1.0)
+ q.put(control_cmd_gen(f"brake_{1.0}"))
elif c == 'd':
- q.put("steer_%f" % 0.15)
+ q.put(control_cmd_gen(f"steer_{0.15}"))
elif c == 'z':
- q.put("blinker_left")
+ q.put(control_cmd_gen("blinker_left"))
elif c == 'x':
- q.put("blinker_right")
+ q.put(control_cmd_gen("blinker_right"))
elif c == 'i':
- q.put("ignition")
+ q.put(control_cmd_gen("ignition"))
elif c == 'r':
- q.put("reset")
+ q.put(control_cmd_gen("reset"))
elif c == 'q':
- q.put("quit")
+ q.put(control_cmd_gen("quit"))
break
else:
print_keyboard_help()
@@ -92,7 +95,7 @@ def test(q: 'Queue[str]') -> NoReturn:
if __name__ == '__main__':
from multiprocessing import Process, Queue
- q: Queue[str] = Queue()
+ q: 'Queue[QueueMessage]' = Queue()
p = Process(target=test, args=(q,))
p.daemon = True
p.start()
diff --git a/tools/sim/lib/manual_ctrl.py b/tools/sim/lib/manual_ctrl.py
index 8a72296538..972f7023bb 100755
--- a/tools/sim/lib/manual_ctrl.py
+++ b/tools/sim/lib/manual_ctrl.py
@@ -6,6 +6,8 @@ import struct
from fcntl import ioctl
from typing import NoReturn
+from openpilot.tools.sim.bridge.common import control_cmd_gen
+
# Iterate over the joystick devices.
print('Available devices:')
for fn in os.listdir('/dev/input'):
@@ -153,33 +155,33 @@ def wheel_poll_thread(q: 'Queue[str]') -> NoReturn:
fvalue = value / 32767.0
axis_states[axis] = fvalue
normalized = (1 - fvalue) * 50
- q.put(f"throttle_{normalized:f}")
+ q.put(control_cmd_gen(f"throttle_{normalized:f}"))
elif axis == "rz": # brake
fvalue = value / 32767.0
axis_states[axis] = fvalue
normalized = (1 - fvalue) * 50
- q.put(f"brake_{normalized:f}")
+ q.put(control_cmd_gen(f"brake_{normalized:f}"))
elif axis == "x": # steer angle
fvalue = value / 32767.0
axis_states[axis] = fvalue
normalized = fvalue
- q.put(f"steer_{normalized:f}")
+ q.put(control_cmd_gen(f"steer_{normalized:f}"))
elif mtype & 0x01: # buttons
if value == 1: # press down
if number in [0, 19]: # X
- q.put("cruise_down")
+ q.put(control_cmd_gen("cruise_down"))
elif number in [3, 18]: # triangle
- q.put("cruise_up")
+ q.put(control_cmd_gen("cruise_up"))
elif number in [1, 6]: # square
- q.put("cruise_cancel")
+ q.put(control_cmd_gen("cruise_cancel"))
elif number in [10, 21]: # R3
- q.put("reverse_switch")
+ q.put(control_cmd_gen("reverse_switch"))
if __name__ == '__main__':
from multiprocessing import Process, Queue
diff --git a/tools/sim/scenarios/metadrive/stay_in_lane.py b/tools/sim/scenarios/metadrive/stay_in_lane.py
deleted file mode 100755
index 683ce55162..0000000000
--- a/tools/sim/scenarios/metadrive/stay_in_lane.py
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/usr/bin/env python
-
-from multiprocessing import Queue
-
-from metadrive.component.sensors.base_camera import _cuda_enable
-from metadrive.component.map.pg_map import MapGenerateMethod
-
-from openpilot.tools.sim.bridge.common import SimulatorBridge
-from openpilot.tools.sim.bridge.metadrive.metadrive_common import RGBCameraRoad, RGBCameraWide
-from openpilot.tools.sim.bridge.metadrive.metadrive_world import MetaDriveWorld
-from openpilot.tools.sim.lib.camerad import W, H
-
-
-def create_map():
- return dict(
- type=MapGenerateMethod.PG_MAP_FILE,
- lane_num=2,
- lane_width=3.5,
- config=[
- {
- "id": "S",
- "pre_block_socket_index": 0,
- "length": 60,
- },
- {
- "id": "C",
- "pre_block_socket_index": 0,
- "length": 60,
- "radius": 600,
- "angle": 45,
- "dir": 0,
- },
- ]
- )
-
-
-class MetaDriveBridge(SimulatorBridge):
- TICKS_PER_FRAME = 5
-
- def __init__(self, world_status_q, dual_camera, high_quality):
- self.world_status_q = world_status_q
- self.should_render = False
-
- super().__init__(dual_camera, high_quality)
-
- def spawn_world(self):
- sensors = {
- "rgb_road": (RGBCameraRoad, W, H, )
- }
-
- if self.dual_camera:
- sensors["rgb_wide"] = (RGBCameraWide, W, H)
-
- config = dict(
- use_render=self.should_render,
- vehicle_config=dict(
- enable_reverse=False,
- image_source="rgb_road",
- ),
- sensors=sensors,
- image_on_cuda=_cuda_enable,
- image_observation=True,
- interface_panel=[],
- out_of_route_done=True,
- on_continuous_line_done=True,
- crash_vehicle_done=True,
- crash_object_done=True,
- arrive_dest_done=True,
- traffic_density=0.0,
- map_config=create_map(),
- map_region_size=2048,
- decision_repeat=1,
- physics_world_step_size=self.TICKS_PER_FRAME/100,
- preload_models=False
- )
-
- return MetaDriveWorld(world_status_q, config, self.dual_camera)
-
-
-if __name__ == "__main__":
- command_queue: Queue = Queue()
- world_status_q: Queue = Queue()
- simulator_bridge = MetaDriveBridge(world_status_q, True, False)
- simulator_process = simulator_bridge.run(command_queue)
-
- while True:
- world_status = world_status_q.get()
- print(f"World Status: {str(world_status)}")
- if world_status["status"] == "terminating":
- break
-
- simulator_process.join()
diff --git a/tools/sim/tests/conftest.py b/tools/sim/tests/conftest.py
new file mode 100644
index 0000000000..ddf6635276
--- /dev/null
+++ b/tools/sim/tests/conftest.py
@@ -0,0 +1,8 @@
+import pytest
+
+def pytest_addoption(parser):
+ parser.addoption("--test_duration", action="store", default=60, type=int, help="Seconds to run metadrive drive")
+
+@pytest.fixture
+def test_duration(request):
+ return request.config.getoption("--test_duration")
diff --git a/tools/sim/tests/test_metadrive_bridge.py b/tools/sim/tests/test_metadrive_bridge.py
index 6cb8e1465e..f06110184b 100755
--- a/tools/sim/tests/test_metadrive_bridge.py
+++ b/tools/sim/tests/test_metadrive_bridge.py
@@ -1,15 +1,22 @@
#!/usr/bin/env python3
import pytest
-import unittest
+import warnings
+
+# Since metadrive depends on pkg_resources, and pkg_resources is deprecated as an API
+warnings.filterwarnings("ignore", category=DeprecationWarning)
from openpilot.tools.sim.bridge.metadrive.metadrive_bridge import MetaDriveBridge
from openpilot.tools.sim.tests.test_sim_bridge import TestSimBridgeBase
@pytest.mark.slow
+@pytest.mark.filterwarnings("ignore::pyopencl.CompilerWarning") # Unimportant warning of non-empty compile log
class TestMetaDriveBridge(TestSimBridgeBase):
+ @pytest.fixture(autouse=True)
+ def setup_create_bridge(self, test_duration):
+ # run bridge test for at least 60s, since not-moving check runs every 30s
+ if test_duration < 60:
+ test_duration = 60
+ self.test_duration = test_duration
+
def create_bridge(self):
- return MetaDriveBridge(False, False)
-
-
-if __name__ == "__main__":
- unittest.main()
+ return MetaDriveBridge(False, False, self.test_duration, True)
diff --git a/tools/sim/tests/test_sim_bridge.py b/tools/sim/tests/test_sim_bridge.py
index d9653d5cfd..aaa90f153f 100644
--- a/tools/sim/tests/test_sim_bridge.py
+++ b/tools/sim/tests/test_sim_bridge.py
@@ -1,25 +1,26 @@
import os
import subprocess
import time
-import unittest
+import pytest
from multiprocessing import Queue
from cereal import messaging
from openpilot.common.basedir import BASEDIR
+from openpilot.tools.sim.bridge.common import QueueMessageType
SIM_DIR = os.path.join(BASEDIR, "tools/sim")
-class TestSimBridgeBase(unittest.TestCase):
+class TestSimBridgeBase:
@classmethod
- def setUpClass(cls):
+ def setup_class(cls):
if cls is TestSimBridgeBase:
- raise unittest.SkipTest("Don't run this base class, run test_metadrive_bridge.py instead")
+ raise pytest.skip("Don't run this base class, run test_metadrive_bridge.py instead")
- def setUp(self):
+ def setup_method(self):
self.processes = []
- def test_engage(self):
+ def test_driving(self):
# Startup manager and bridge.py. Check processes are running, then engage and verify.
p_manager = subprocess.Popen("./launch_openpilot.sh", cwd=SIM_DIR)
self.processes.append(p_manager)
@@ -36,7 +37,7 @@ class TestSimBridgeBase(unittest.TestCase):
start_waiting = time.monotonic()
while not bridge.started.value and time.monotonic() < start_waiting + max_time_per_step:
time.sleep(0.1)
- self.assertEqual(p_bridge.exitcode, None, f"Bridge process should be running, but exited with code {p_bridge.exitcode}")
+ assert p_bridge.exitcode is None, f"Bridge process should be running, but exited with code {p_bridge.exitcode}"
start_time = time.monotonic()
no_car_events_issues_once = False
@@ -52,8 +53,8 @@ class TestSimBridgeBase(unittest.TestCase):
no_car_events_issues_once = True
break
- self.assertTrue(no_car_events_issues_once,
- f"Failed because no messages received, or CarEvents '{car_event_issues}' or processes not running '{not_running}'")
+ assert no_car_events_issues_once, \
+ f"Failed because no messages received, or CarEvents '{car_event_issues}' or processes not running '{not_running}'"
start_time = time.monotonic()
min_counts_control_active = 100
@@ -68,9 +69,21 @@ class TestSimBridgeBase(unittest.TestCase):
if control_active == min_counts_control_active:
break
- self.assertEqual(min_counts_control_active, control_active, f"Simulator did not engage a minimal of {min_counts_control_active} steps was {control_active}")
+ assert min_counts_control_active == control_active, f"Simulator did not engage a minimal of {min_counts_control_active} steps was {control_active}"
- def tearDown(self):
+ failure_states = []
+ while bridge.started.value:
+ continue
+
+ while not q.empty():
+ state = q.get()
+ if state.type == QueueMessageType.TERMINATION_INFO:
+ done_info = state.info
+ failure_states = [done_state for done_state in done_info if done_state != "timeout" and done_info[done_state]]
+ break
+ assert len(failure_states) == 0, f"Simulator fails to finish a loop. Failure states: {failure_states}"
+
+ def teardown_method(self):
print("Test shutting down. CommIssues are acceptable")
for p in reversed(self.processes):
p.terminate()
@@ -80,7 +93,3 @@ class TestSimBridgeBase(unittest.TestCase):
p.wait(15)
else:
p.join(15)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tools/webcam/Dockerfile b/tools/webcam/Dockerfile
deleted file mode 100644
index 78a25e836d..0000000000
--- a/tools/webcam/Dockerfile
+++ /dev/null
@@ -1,41 +0,0 @@
-FROM ghcr.io/commaai/openpilot-base:latest
-
-ENV PYTHONUNBUFFERED 1
-ENV PYTHONPATH /tmp/openpilot:${PYTHONPATH}
-
-# Install opencv
-ENV OPENCV_VERSION '4.2.0'
-ENV DEBIAN_FRONTEND=noninteractive
-RUN apt-get update && apt-get install -y --no-install-recommends \
- libvtk6-dev \
- libdc1394-22-dev \
- libavcodec-dev \
- libavformat-dev \
- libswscale-dev \
- libtheora-dev \
- libvorbis-dev \
- libxvidcore-dev \
- libx264-dev \
- yasm \
- libopencore-amrnb-dev \
- libopencore-amrwb-dev \
- libv4l-dev \
- libxine2-dev \
- libtbb-dev \
- && rm -rf /var/lib/apt/lists/* && \
-
- mkdir /tmp/opencv_build && \
- cd /tmp/opencv_build && \
-
- curl -L -O https://github.com/opencv/opencv/archive/${OPENCV_VERSION}.tar.gz && \
- tar -xvf ${OPENCV_VERSION}.tar.gz && \
- mv opencv-${OPENCV_VERSION} OpenCV && \
- cd OpenCV && mkdir build && cd build && \
- cmake -DWITH_OPENGL=ON -DFORCE_VTK=ON -DWITH_TBB=ON -DWITH_GDAL=ON \
- -DWITH_XINE=ON -DENABLE_PRECOMPILED_HEADERS=OFF -DBUILD_TESTS=OFF \
- -DBUILD_PERF_TESTS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_opencv_apps=OFF .. && \
- make -j8 && \
- make install && \
- ldconfig && \
-
- cd / && rm -rf /tmp/*
diff --git a/tools/webcam/README.md b/tools/webcam/README.md
index 709e8514c7..7334d87b38 100644
--- a/tools/webcam/README.md
+++ b/tools/webcam/README.md
@@ -14,10 +14,7 @@ cd ~
git clone https://github.com/commaai/openpilot.git
```
- Follow [this readme](https://github.com/commaai/openpilot/tree/master/tools) to install the requirements
-- Add line "export PYTHONPATH=$HOME/openpilot" to your ~/.bashrc
-- Install tensorflow 2.2 and nvidia drivers: nvidia-xxx/cuda10.0/cudnn7.6.5
- Install [OpenCL Driver](https://registrationcenter-download.intel.com/akdlm/irc_nas/vcp/15532/l_opencl_p_18.1.0.015.tgz)
-- Install [OpenCV4](https://www.pyimagesearch.com/2018/08/15/how-to-install-opencv-4-on-ubuntu/) (ignore the Python part)
## Build openpilot for webcam
```
@@ -32,7 +29,7 @@ USE_WEBCAM=1 scons -j$(nproc)
## GO
```
-cd ~/openpilot/selfdrive/manager
+cd ~/openpilot/system/manager
NOSENSOR=1 USE_WEBCAM=1 ./manager.py
```
- Start the car, then the UI should show the road webcam's view