diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml index cedeee1741..edb058d7d1 100644 --- a/.github/workflows/auto_pr_review.yaml +++ b/.github/workflows/auto_pr_review.yaml @@ -12,12 +12,12 @@ jobs: issues: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: false # Label PRs - - uses: actions/labeler@v5.0.0 + - uses: actions/labeler@v6 with: dot: true configuration-path: .github/labeler.yaml @@ -55,13 +55,13 @@ jobs: repo: context.repo.repo, issue_number: prNumber }); - + const hasDevC3Label = labels.some(label => label.name === process.env.PR_LABEL); const hasTrustLabel = labels.some(label => label.name === process.env.TRUST_FORK_PR_LABEL); - + console.log(`PR #${prNumber} has ${process.env.PR_LABEL} label: ${hasDevC3Label}`); console.log(`PR #${prNumber} has ${process.env.TRUST_FORK_PR_LABEL} label: ${hasTrustLabel}`); - + core.setOutput('has-dev', hasDevC3Label ? 'true' : 'false'); core.setOutput('has-trust', hasTrustLabel ? 'true' : 'false'); @@ -81,7 +81,7 @@ jobs: }); console.log(`Removed '${process.env.TRUST_FORK_PR_LABEL}' label from PR #${prNumber} as it received new commits`); - + // Add a comment to the PR await github.rest.issues.createComment({ owner: context.repo.owner, diff --git a/.github/workflows/badges.yaml b/.github/workflows/badges.yaml index 6cd7a4687b..2d9ba46d0a 100644 --- a/.github/workflows/badges.yaml +++ b/.github/workflows/badges.yaml @@ -17,7 +17,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry diff --git a/.github/workflows/ci_weekly_report.yaml b/.github/workflows/ci_weekly_report.yaml index 1d3563fb18..eea3cb3f4d 100644 --- a/.github/workflows/ci_weekly_report.yaml +++ b/.github/workflows/ci_weekly_report.yaml @@ -41,7 +41,7 @@ jobs: if: always() && github.repository == 'commaai/openpilot' steps: - name: Get job results - uses: actions/github-script@v7 + uses: actions/github-script@v8 id: get-job-results with: script: | diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index ce225034d9..27d36f9a4e 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -22,7 +22,7 @@ jobs: steps: - uses: commaai/timeout@v1 - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true @@ -34,7 +34,7 @@ jobs: mkdocs build # Push to docs.comma.ai - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 if: github.ref == 'refs/heads/master' && github.repository == 'sunnypilot/sunnypilot' with: path: openpilot-docs diff --git a/.github/workflows/jenkins-pr-trigger.yaml b/.github/workflows/jenkins-pr-trigger.yaml index 14e2fdf49b..f8a53c5ae0 100644 --- a/.github/workflows/jenkins-pr-trigger.yaml +++ b/.github/workflows/jenkins-pr-trigger.yaml @@ -15,7 +15,7 @@ jobs: steps: - name: Check for trigger phrase id: check_comment - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const triggerPhrase = "trigger-jenkins"; @@ -35,7 +35,7 @@ jobs: - name: Checkout repository if: steps.check_comment.outputs.result == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: refs/pull/${{ github.event.issue.number }}/head @@ -49,7 +49,7 @@ jobs: - name: Delete trigger comment if: steps.check_comment.outputs.result == 'true' && always() - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | await github.rest.issues.deleteComment({ diff --git a/.github/workflows/mici_raylib_ui_preview.yaml b/.github/workflows/mici_raylib_ui_preview.yaml index 5552586529..fa51666bb6 100644 --- a/.github/workflows/mici_raylib_ui_preview.yaml +++ b/.github/workflows/mici_raylib_ui_preview.yaml @@ -33,7 +33,7 @@ jobs: pull-requests: write actions: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true @@ -62,7 +62,7 @@ jobs: path: ${{ github.workspace }}/pr_ui - name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4 - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: sunnypilot/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} diff --git a/.github/workflows/model_review.yaml b/.github/workflows/model_review.yaml index 0e1825864c..6b8ce143db 100644 --- a/.github/workflows/model_review.yaml +++ b/.github/workflows/model_review.yaml @@ -16,9 +16,9 @@ jobs: if: github.repository == 'commaai/openpilot' steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Checkout master - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: master path: base diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml index dde290d5c5..a038cdce63 100644 --- a/.github/workflows/prebuilt.yaml +++ b/.github/workflows/prebuilt.yaml @@ -29,7 +29,7 @@ jobs: running-workflow-name: 'build prebuilt' repo-token: ${{ secrets.GITHUB_TOKEN }} check-regexp: ^((?!.*(build master-ci).*).)*$ - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - run: git lfs pull diff --git a/.github/workflows/raylib_ui_preview.yaml b/.github/workflows/raylib_ui_preview.yaml index 99651a231e..7f569f3bf1 100644 --- a/.github/workflows/raylib_ui_preview.yaml +++ b/.github/workflows/raylib_ui_preview.yaml @@ -58,7 +58,7 @@ jobs: path: ${{ github.workspace }}/pr_ui - name: Getting master ui - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: sunnypilot/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b9303045d0..3a6cfa59d0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -30,7 +30,7 @@ jobs: running-workflow-name: 'build __nightly' repo-token: ${{ secrets.GITHUB_TOKEN }} check-regexp: ^((?!.*(build prebuilt).*).)*$ - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 49dae93747..073de88f8e 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -8,14 +8,14 @@ on: env: BASE_IMAGE: sunnypilot-base BUILD: release/ci/docker_build_sp.sh base - RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c + RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c jobs: update_translations: runs-on: ubuntu-latest if: github.repository == 'sunnypilot/sunnypilot' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: ./.github/workflows/setup-with-retry - name: Update translations run: | @@ -39,7 +39,7 @@ jobs: image: ghcr.io/sunnypilot/sunnypilot-base:latest if: github.repository == 'sunnypilot/sunnypilot' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: uv lock diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 38bdb198d6..dab61bf3b4 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -13,7 +13,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@v10 with: exempt-all-milestones: true @@ -34,7 +34,7 @@ jobs: stale_drafts: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@v10 with: exempt-all-milestones: true diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7ae393bb5b..18f01715d0 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -25,7 +25,7 @@ env: DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} BUILD: release/ci/docker_build_sp.sh base - RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c + RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical @@ -41,7 +41,7 @@ jobs: env: STRIPPED_DIR: /tmp/releasepilot steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Getting LFS files @@ -93,7 +93,7 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - name: Setup docker push @@ -109,7 +109,7 @@ jobs: name: build macOS runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV @@ -163,7 +163,7 @@ jobs: env: PYTHONWARNINGS: default steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry @@ -180,7 +180,7 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry @@ -206,14 +206,14 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry id: setup-step - name: Cache test routes id: dependency-cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .ci_cache/comma_download_cache key: proc-replay-${{ hashFiles('selfdrive/test/process_replay/ref_commit', 'selfdrive/test/process_replay/test_processes.py') }} @@ -229,7 +229,7 @@ jobs: id: print-diff if: always() run: cat selfdrive/test/process_replay/diff.txt - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 if: always() continue-on-error: true with: @@ -256,7 +256,7 @@ jobs: || fromJSON('["ubuntu-24.04"]') }} if: false # FIXME: Started to timeout recently steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry @@ -280,7 +280,7 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry @@ -292,7 +292,7 @@ jobs: source selfdrive/test/setup_xvfb.sh && python3 selfdrive/ui/tests/test_ui/raylib_screenshots.py" - name: Upload Raylib UI Report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} path: selfdrive/ui/tests/test_ui/raylib_report/screenshots @@ -306,7 +306,7 @@ jobs: && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') || fromJSON('["ubuntu-24.04"]') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: true - uses: ./.github/workflows/setup-with-retry @@ -318,7 +318,7 @@ jobs: source selfdrive/test/setup_xvfb.sh && WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py" - name: Upload Raylib UI Report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: mici-raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} path: selfdrive/ui/tests/diff/report diff --git a/.gitignore b/.gitignore index b06d620d4e..8a32f23604 100644 --- a/.gitignore +++ b/.gitignore @@ -99,6 +99,10 @@ Pipfile .history .ionide +.claude/ +PLAN.md +TASK.md + ### JetBrains ### !.idea/customTargets.xml !.idea/tools/* diff --git a/SConstruct b/SConstruct index b2abae8d1b..9f54e2a560 100644 --- a/SConstruct +++ b/SConstruct @@ -14,7 +14,6 @@ Decider('MD5-timestamp') SetOption('num_jobs', max(1, int(os.cpu_count()/2))) -AddOption('--kaitai', action='store_true', help='Regenerate kaitai struct parsers') AddOption('--asan', action='store_true', help='turn on ASAN') AddOption('--ubsan', action='store_true', help='turn on UBSan') AddOption('--mutation', action='store_true', help='generate mutation-ready code') @@ -203,7 +202,6 @@ SConscript(['rednose/SConscript']) # Build system services SConscript([ - 'system/ubloxd/SConscript', 'system/loggerd/SConscript', ]) diff --git a/cereal/log.capnp b/cereal/log.capnp index b2a9a789e6..8ccd86f0ed 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2227,9 +2227,9 @@ struct DriverMonitoringState @0xb83cda094a1da284 { isActiveMode @16 :Bool; isRHD @4 :Bool; uncertainCount @19 :UInt32; - phoneProbOffset @20 :Float32; - phoneProbValidCount @21 :UInt32; + phoneProbOffsetDEPRECATED @20 :Float32; + phoneProbValidCountDEPRECATED @21 :UInt32; isPreviewDEPRECATED @15 :Bool; rhdCheckedDEPRECATED @5 :Bool; eventsDEPRECATED @0 :List(Car.OnroadEventDEPRECATED); diff --git a/cereal/messaging/__init__.py b/cereal/messaging/__init__.py index d5033cd634..2c925b4cc4 100644 --- a/cereal/messaging/__init__.py +++ b/cereal/messaging/__init__.py @@ -1,10 +1,8 @@ # must be built with scons -from msgq.ipc_pyx import Context, Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \ - set_fake_prefix, get_fake_prefix, delete_fake_prefix, wait_for_one_event -from msgq.ipc_pyx import MultiplePublishersError, IpcError -from msgq import fake_event_handle, drain_sock_raw +from msgq import fake_event_handle, drain_sock_raw, MultiplePublishersError, IpcError, \ + Context, Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \ + set_fake_prefix, get_fake_prefix, delete_fake_prefix, wait_for_one_event import msgq - import os import capnp import time diff --git a/cereal/messaging/bridge.cc b/cereal/messaging/bridge.cc index 69ecd188e1..fb92c575c9 100644 --- a/cereal/messaging/bridge.cc +++ b/cereal/messaging/bridge.cc @@ -33,7 +33,8 @@ void zmq_to_msgq(const std::vector &endpoints, const std::string &i for (auto endpoint : endpoints) { auto pub_sock = new MSGQPubSocket(); auto sub_sock = new ZMQSubSocket(); - pub_sock->connect(pub_context.get(), endpoint); + size_t queue_size = services.at(endpoint).queue_size; + pub_sock->connect(pub_context.get(), endpoint, true, queue_size); sub_sock->connect(sub_context.get(), endpoint, ip, false); poller->registerSocket(sub_sock); diff --git a/cereal/messaging/msgq_to_zmq.cc b/cereal/messaging/msgq_to_zmq.cc index ce626f2aad..7f8c738d4d 100644 --- a/cereal/messaging/msgq_to_zmq.cc +++ b/cereal/messaging/msgq_to_zmq.cc @@ -2,6 +2,7 @@ #include +#include "cereal/services.h" #include "common/util.h" extern ExitHandler do_exit; @@ -108,7 +109,8 @@ void MsgqToZmq::zmqMonitorThread() { if (++pair.connected_clients == 1) { // Create new MSGQ subscriber socket and map to ZMQ publisher pair.sub_sock = std::make_unique(); - pair.sub_sock->connect(msgq_context.get(), pair.endpoint, "127.0.0.1"); + size_t queue_size = services.at(pair.endpoint).queue_size; + pair.sub_sock->connect(msgq_context.get(), pair.endpoint, "127.0.0.1", false, true, queue_size); sub2pub[pair.sub_sock.get()] = pair.pub_sock.get(); registerSockets(); } diff --git a/common/i2c.py b/common/i2c.py new file mode 100644 index 0000000000..1dfaa659ad --- /dev/null +++ b/common/i2c.py @@ -0,0 +1,81 @@ +import os +import fcntl +import ctypes + +# I2C constants from /usr/include/linux/i2c-dev.h +I2C_SLAVE = 0x0703 +I2C_SLAVE_FORCE = 0x0706 +I2C_SMBUS = 0x0720 + +# SMBus transfer types +I2C_SMBUS_READ = 1 +I2C_SMBUS_WRITE = 0 +I2C_SMBUS_BYTE_DATA = 2 +I2C_SMBUS_I2C_BLOCK_DATA = 8 + +I2C_SMBUS_BLOCK_MAX = 32 + + +class _I2cSmbusData(ctypes.Union): + _fields_ = [ + ("byte", ctypes.c_uint8), + ("word", ctypes.c_uint16), + ("block", ctypes.c_uint8 * (I2C_SMBUS_BLOCK_MAX + 2)), + ] + + +class _I2cSmbusIoctlData(ctypes.Structure): + _fields_ = [ + ("read_write", ctypes.c_uint8), + ("command", ctypes.c_uint8), + ("size", ctypes.c_uint32), + ("data", ctypes.POINTER(_I2cSmbusData)), + ] + + +class SMBus: + def __init__(self, bus: int): + self._fd = os.open(f'/dev/i2c-{bus}', os.O_RDWR) + + def __enter__(self) -> 'SMBus': + return self + + def __exit__(self, *args) -> None: + self.close() + + def close(self) -> None: + if hasattr(self, '_fd') and self._fd >= 0: + os.close(self._fd) + self._fd = -1 + + def _set_address(self, addr: int, force: bool = False) -> None: + ioctl_arg = I2C_SLAVE_FORCE if force else I2C_SLAVE + fcntl.ioctl(self._fd, ioctl_arg, addr) + + def _smbus_access(self, read_write: int, command: int, size: int, data: _I2cSmbusData) -> None: + ioctl_data = _I2cSmbusIoctlData(read_write, command, size, ctypes.pointer(data)) + fcntl.ioctl(self._fd, I2C_SMBUS, ioctl_data) + + def read_byte_data(self, addr: int, register: int, force: bool = False) -> int: + self._set_address(addr, force) + data = _I2cSmbusData() + self._smbus_access(I2C_SMBUS_READ, register, I2C_SMBUS_BYTE_DATA, data) + return int(data.byte) + + def write_byte_data(self, addr: int, register: int, value: int, force: bool = False) -> None: + self._set_address(addr, force) + data = _I2cSmbusData() + data.byte = value & 0xFF + self._smbus_access(I2C_SMBUS_WRITE, register, I2C_SMBUS_BYTE_DATA, data) + + def read_i2c_block_data(self, addr: int, register: int, length: int, force: bool = False) -> list[int]: + self._set_address(addr, force) + if not (0 <= length <= I2C_SMBUS_BLOCK_MAX): + raise ValueError(f"length must be 0..{I2C_SMBUS_BLOCK_MAX}") + + data = _I2cSmbusData() + data.block[0] = length + self._smbus_access(I2C_SMBUS_READ, register, I2C_SMBUS_I2C_BLOCK_DATA, data) + read_len = int(data.block[0]) or length + read_len = min(read_len, length) + return [int(b) for b in data.block[1 : read_len + 1]] diff --git a/common/model.h b/common/model.h index 156bd00e7f..444727be93 100644 --- a/common/model.h +++ b/common/model.h @@ -1 +1 @@ -#define DEFAULT_MODEL "WMI (Default)" +#define DEFAULT_MODEL "CD210 (Default)" diff --git a/common/utils.py b/common/utils.py index caa9a57958..ccc6719f5f 100644 --- a/common/utils.py +++ b/common/utils.py @@ -10,6 +10,27 @@ import zstandard as zstd LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change +class Timer: + """Simple lap timer for profiling sequential operations.""" + + def __init__(self): + self._start = self._lap = time.monotonic() + self._sections = {} + + def lap(self, name): + now = time.monotonic() + self._sections[name] = now - self._lap + self._lap = now + + @property + def total(self): + return time.monotonic() - self._start + + def fmt(self, duration): + parts = ", ".join(f"{k}={v:.2f}s" + (f" ({duration/v:.0f}x)" if k == 'render' and v > 0 else "") for k, v in self._sections.items()) + total = self.total + realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" + return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" def sudo_write(val: str, path: str) -> None: try: diff --git a/docs/CARS.md b/docs/CARS.md index 5c659d284e..a1f50e41d0 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,22 +4,23 @@ 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. -# 341 Supported Cars +# 344 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Acura|ILX 2019|All|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Acura|MDX 2025|All except Type S|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Acura|MDX 2025-26|All except Type S|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Acura|RDX 2019-21|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Acura|TLX 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Acura|TLX 2025|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| |Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| |Chevrolet|Bolt EV Non-ACC 2017|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| @@ -34,33 +35,33 @@ A supported vehicle is one that just works when you install a comma device. All |Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None||| -|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ford|Focus 2018[2](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Focus Hybrid 2018[2](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Ford|Kuga Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Kuga Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Ford|Mustang Mach-E 2021-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Mustang Mach-E 2021-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Genesis|G70 2018|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Genesis|G70 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Genesis|G70 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -95,7 +96,7 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|15 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|CR-V 2023-26|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Honda|CR-V Hybrid 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|CR-V Hybrid 2023-26|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|e 2020|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -106,6 +107,7 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|N-Box 2018|All|openpilot available[1](#footnotes)|0 mph|11 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Odyssey 2021-26|All|openpilot available[1](#footnotes)|0 mph|43 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Odyssey (Taiwan) 2018-19|Honda Sensing|openpilot|19 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Passport 2026|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -211,6 +213,7 @@ A supported vehicle is one that just works when you install a comma device. All |Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|LC 2024-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|LS 2018|All except Lexus Safety System+ A|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|NX 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|NX Hybrid 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -226,21 +229,21 @@ A supported vehicle is one that just works when you install a comma device. All |Lexus|UX Hybrid 2019-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Nissan[5](#footnotes)|Altima 2019-20, 2024|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan B connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Nissan[5](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Nissan[5](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Nissan[5](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#footnotes)|Altima 2019-20, 2024|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Subaru|Ascent 2019-21|All[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| |Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| |Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| @@ -255,19 +258,19 @@ A supported vehicle is one that just works when you install a comma device. All |Subaru|Outback 2020-22|All[6](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| |Subaru|XV 2018-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| |Subaru|XV 2020-21|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Å koda|Fabia 2022-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Å koda|Kamiq 2021-23[11,13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Å koda|Karoq 2019-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Å koda|Kodiaq 2017-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Å koda|Octavia 2015-19[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Å koda|Octavia RS 2016[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Å koda|Octavia Scout 2017-19[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Å koda|Scala 2020-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Å koda|Superb 2015-22[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[9](#footnotes)|Model 3 (with HW3) 2019-23[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[9](#footnotes)|Model 3 (with HW4) 2024-25[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[9](#footnotes)|Model Y (with HW3) 2020-23[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[9](#footnotes)|Model Y (with HW4) 2024-25[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Å koda|Fabia 2022-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Å koda|Kamiq 2021-23[11,13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Å koda|Karoq 2019-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Å koda|Kodiaq 2017-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Å koda|Octavia 2015-19[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Å koda|Octavia RS 2016[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Å koda|Octavia Scout 2017-19[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Å koda|Scala 2020-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Å koda|Superb 2015-22[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model 3 (with HW3) 2019-23[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model 3 (with HW4) 2024-25[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model Y (with HW3) 2020-23[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model Y (with HW4) 2024-25[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Avalon 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -313,42 +316,42 @@ A supported vehicle is one that just works when you install a comma device. All |Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|RAV4 Hybrid 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Sienna 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Jetta 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Passat 2015-22[12](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| -|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat 2015-22[12](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| ### Footnotes 1openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `nightly-dev`.
diff --git a/opendbc_repo b/opendbc_repo index a653199681..1abdb9872f 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit a653199681e95be113aea491bb7a6023c8b73eb1 +Subproject commit 1abdb9872f22c361322ac9e29d376c35233ba890 diff --git a/panda b/panda index f9cdec7f7b..ed8a6f9ec2 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit f9cdec7f7bf44b5335d8b2345993ae533fbb010b +Subproject commit ed8a6f9ec2849c144ab1b4a4dde2ba2c30ae3b1c diff --git a/pyproject.toml b/pyproject.toml index 185a9a5222..1e62b804c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "openpilot" -requires-python = ">= 3.11, < 3.13" +requires-python = ">= 3.12.3, < 3.13" license = {text = "MIT License"} version = "0.1.0" description = "an open source driver assistance system" authors = [ - {name ="Vehicle Researcher", email="user@comma.ai"} + {name = "Vehicle Researcher", email="user@comma.ai"} ] dependencies = [ @@ -17,9 +17,6 @@ dependencies = [ "crcmod-plus", # cars + qcomgpsd "tqdm", # cars (fw_versions.py) on start + many one-off uses - # hardwared - "smbus2", # configuring amp - # core "cffi", "scons", @@ -36,9 +33,6 @@ dependencies = [ "pyopenssl < 24.3.0", "pyaudio", - # ubloxd (TODO: just use struct) - "kaitaistruct", - # panda "libusb1", "spidev; platform_system == 'Linux'", @@ -58,7 +52,6 @@ dependencies = [ # acados deps "casadi >=3.6.6", # 3.12 fixed in 3.6.6 - "future-fstrings", # joystickd "inputs", @@ -75,12 +68,12 @@ dependencies = [ "raylib > 5.5.0.3", "qrcode", "mapbox-earcut", + "jeepney", ] [project.optional-dependencies] docs = [ "Jinja2", - "natsort", "mkdocs", ] @@ -94,7 +87,6 @@ testing = [ # https://github.com/pytest-dev/pytest-xdist/pull/1229 "pytest-xdist @ git+https://github.com/sshane/pytest-xdist@2b4372bd62699fb412c4fe2f95bf9f01bd2018da", "pytest-timeout", - "pytest-randomly", "pytest-asyncio", "pytest-mock", "pytest-repeat", @@ -108,18 +100,12 @@ dev = [ "azure-identity", "azure-storage-blob", "dictdiffer", - "jeepney", "matplotlib", "opencv-python-headless", "parameterized >=0.8, <0.9", "pyautogui", - "pyopencl", - "pytools>=2025.1.6; platform_machine != 'aarch64'", "pywinctl", - "pyprof2calltree", "tabulate", - "types-requests", - "types-tabulate", ] tools = [ @@ -156,19 +142,9 @@ markers = [ testpaths = [ "common", "selfdrive", - "system/manager", - "system/updated", - "system/athena", - "system/camerad", - "system/hardware", - "system/loggerd", - "system/tests", - "system/ubloxd", - "system/webrtc", - "tools/lib/tests", - "tools/replay", - "tools/cabana", - "cereal/messaging/tests", + "system", + "tools", + "cereal", "sunnypilot", ] @@ -179,7 +155,7 @@ ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,w builtin = "clear,rare,informal,code,names,en-GB_to_en-US" skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html" -# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml +# https://docs.astral.sh/ruff/configuration/#using-pyprojecttoml [tool.ruff] indent-width = 2 lint.select = [ diff --git a/scripts/ci_results.py b/scripts/ci_results.py new file mode 100755 index 0000000000..a133541c69 --- /dev/null +++ b/scripts/ci_results.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""Fetch CI results from GitHub Actions and Jenkins.""" + +import argparse +import json +import subprocess +import time +import urllib.error +import urllib.request +from datetime import datetime + +JENKINS_URL = "https://jenkins.comma.life" +DEFAULT_TIMEOUT = 1800 # 30 minutes +POLL_INTERVAL = 30 # seconds +LOG_TAIL_LINES = 10 # lines of log to include for failed jobs + + +def get_git_info(): + branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], text=True).strip() + commit = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() + return branch, commit + + +def get_github_actions_status(commit_sha): + result = subprocess.run( + ["gh", "run", "list", "--commit", commit_sha, "--workflow", "tests.yaml", "--json", "databaseId,status,conclusion"], + capture_output=True, text=True, check=True + ) + runs = json.loads(result.stdout) + if not runs: + return None, None + + run_id = runs[0]["databaseId"] + result = subprocess.run( + ["gh", "run", "view", str(run_id), "--json", "jobs"], + capture_output=True, text=True, check=True + ) + data = json.loads(result.stdout) + jobs = {job["name"]: {"status": job["status"], "conclusion": job["conclusion"], + "duration": format_duration(job) if job["conclusion"] not in ("skipped", None) and job.get("startedAt") else "", + "id": job["databaseId"]} + for job in data.get("jobs", [])} + return jobs, run_id + + +def get_github_job_log(run_id, job_id): + result = subprocess.run( + ["gh", "run", "view", str(run_id), "--job", str(job_id), "--log-failed"], + capture_output=True, text=True + ) + lines = result.stdout.strip().split('\n') + return '\n'.join(lines[-LOG_TAIL_LINES:]) if len(lines) > LOG_TAIL_LINES else result.stdout.strip() + + +def format_duration(job): + start = datetime.fromisoformat(job["startedAt"].replace("Z", "+00:00")) + end = datetime.fromisoformat(job["completedAt"].replace("Z", "+00:00")) + secs = int((end - start).total_seconds()) + return f"{secs // 60}m {secs % 60}s" + + +def get_jenkins_status(branch, commit_sha): + base_url = f"{JENKINS_URL}/job/openpilot/job/{branch}" + try: + # Get list of recent builds + with urllib.request.urlopen(f"{base_url}/api/json?tree=builds[number,url]", timeout=10) as resp: + builds = json.loads(resp.read().decode()).get("builds", []) + + # Find build matching commit + for build in builds[:20]: # check last 20 builds + with urllib.request.urlopen(f"{build['url']}api/json", timeout=10) as resp: + data = json.loads(resp.read().decode()) + for action in data.get("actions", []): + if action.get("_class") == "hudson.plugins.git.util.BuildData": + build_sha = action.get("lastBuiltRevision", {}).get("SHA1", "") + if build_sha.startswith(commit_sha) or commit_sha.startswith(build_sha): + # Get stages info + stages = [] + try: + with urllib.request.urlopen(f"{build['url']}wfapi/describe", timeout=10) as resp2: + wf_data = json.loads(resp2.read().decode()) + stages = [{"name": s["name"], "status": s["status"]} for s in wf_data.get("stages", [])] + except urllib.error.HTTPError: + pass + return { + "number": data["number"], + "in_progress": data.get("inProgress", False), + "result": data.get("result"), + "url": data.get("url", ""), + "stages": stages, + } + return None # no build found for this commit + except urllib.error.HTTPError: + return None # branch doesn't exist on Jenkins + + +def get_jenkins_log(build_url): + url = f"{build_url}consoleText" + with urllib.request.urlopen(url, timeout=30) as resp: + text = resp.read().decode(errors='replace') + lines = text.strip().split('\n') + return '\n'.join(lines[-LOG_TAIL_LINES:]) if len(lines) > LOG_TAIL_LINES else text.strip() + + +def is_complete(gh_status, jenkins_status): + gh_done = gh_status is None or all(j["status"] == "completed" for j in gh_status.values()) + jenkins_done = jenkins_status is None or not jenkins_status.get("in_progress", True) + return gh_done and jenkins_done + + +def status_icon(status, conclusion=None): + if status == "completed": + return ":white_check_mark:" if conclusion == "success" else ":x:" + return ":hourglass:" if status == "in_progress" else ":grey_question:" + + +def format_markdown(gh_status, gh_run_id, jenkins_status, commit_sha, branch): + lines = ["# CI Results", "", + f"**Branch**: {branch}", + f"**Commit**: {commit_sha[:7]}", + f"**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", ""] + + lines.extend(["## GitHub Actions", "", "| Job | Status | Duration |", "|-----|--------|----------|"]) + failed_gh_jobs = [] + if gh_status: + for job_name, job in gh_status.items(): + icon = status_icon(job["status"], job.get("conclusion")) + conclusion = job.get("conclusion") or job["status"] + lines.append(f"| {job_name} | {icon} {conclusion} | {job.get('duration', '')} |") + if job.get("conclusion") == "failure": + failed_gh_jobs.append((job_name, job.get("id"))) + else: + lines.append("| - | No workflow runs found | |") + + lines.extend(["", "## Jenkins", "", "| Stage | Status |", "|-------|--------|"]) + failed_jenkins_stages = [] + if jenkins_status: + stages = jenkins_status.get("stages", []) + if stages: + for stage in stages: + icon = ":white_check_mark:" if stage["status"] == "SUCCESS" else ( + ":x:" if stage["status"] == "FAILED" else ":hourglass:") + lines.append(f"| {stage['name']} | {icon} {stage['status'].lower()} |") + if stage["status"] == "FAILED": + failed_jenkins_stages.append(stage["name"]) + # Show overall build status if still in progress + if jenkins_status["in_progress"]: + lines.append("| (build in progress) | :hourglass: in_progress |") + else: + icon = ":hourglass:" if jenkins_status["in_progress"] else ( + ":white_check_mark:" if jenkins_status["result"] == "SUCCESS" else ":x:") + status = "in progress" if jenkins_status["in_progress"] else (jenkins_status["result"] or "unknown") + lines.append(f"| #{jenkins_status['number']} | {icon} {status.lower()} |") + if jenkins_status.get("url"): + lines.append(f"\n[View build]({jenkins_status['url']})") + else: + lines.append("| - | No builds found for branch |") + + if failed_gh_jobs or failed_jenkins_stages: + lines.extend(["", "## Failure Logs", ""]) + + for job_name, job_id in failed_gh_jobs: + lines.append(f"### GitHub Actions: {job_name}") + log = get_github_job_log(gh_run_id, job_id) + lines.extend(["", "```", log, "```", ""]) + + for stage_name in failed_jenkins_stages: + lines.append(f"### Jenkins: {stage_name}") + log = get_jenkins_log(jenkins_status["url"]) + lines.extend(["", "```", log, "```", ""]) + + return "\n".join(lines) + "\n" + + +def main(): + parser = argparse.ArgumentParser(description="Fetch CI results from GitHub Actions and Jenkins") + parser.add_argument("--wait", action="store_true", help="Wait for CI to complete") + parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="Timeout in seconds (default: 1800)") + parser.add_argument("-o", "--output", default="ci_results.md", help="Output file (default: ci_results.md)") + parser.add_argument("--branch", help="Branch to check (default: current branch)") + parser.add_argument("--commit", help="Commit SHA to check (default: HEAD)") + args = parser.parse_args() + + branch, commit = get_git_info() + branch = args.branch or branch + commit = args.commit or commit + print(f"Fetching CI results for {branch} @ {commit[:7]}") + + start_time = time.monotonic() + while True: + gh_status, gh_run_id = get_github_actions_status(commit) + jenkins_status = get_jenkins_status(branch, commit) if branch != "HEAD" else None + + if not args.wait or is_complete(gh_status, jenkins_status): + break + + elapsed = time.monotonic() - start_time + if elapsed >= args.timeout: + print(f"Timeout after {int(elapsed)}s") + break + + print(f"CI still running, waiting {POLL_INTERVAL}s... ({int(elapsed)}s elapsed)") + time.sleep(POLL_INTERVAL) + + content = format_markdown(gh_status, gh_run_id, jenkins_status, commit, branch) + with open(args.output, "w") as f: + f.write(content) + print(f"Results written to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/selfdrive/assets/icons_mici/adb_short.png b/selfdrive/assets/icons_mici/adb_short.png new file mode 100644 index 0000000000..c49226c858 --- /dev/null +++ b/selfdrive/assets/icons_mici/adb_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:263598da73c577c01cebd31ae78f45969ef8b335be1a5f55d54a696bb2982c0a +size 2062 diff --git a/selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png b/selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png index 0e21bc1b5a..1ff4db45a5 100644 --- a/selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png +++ b/selfdrive/assets/icons_mici/buttons/toggle_dot_disabled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:613af9ed79bb26c60fbd19c094214f0881736c0e293f6d000b530cde0478a273 -size 2470 +oid sha256:89ac033d879beeb0a7fa1919838e0ec64b1a625a4aafc14f7b990c607a79b676 +size 2220 diff --git a/selfdrive/assets/icons_mici/exclamation_point.png b/selfdrive/assets/icons_mici/exclamation_point.png index 246fc015ec..ede3b638bc 100644 --- a/selfdrive/assets/icons_mici/exclamation_point.png +++ b/selfdrive/assets/icons_mici/exclamation_point.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b77579c099c688d1a27f356197fba9c2c8efcf4d391af580b4b29f0e70587919 -size 2086 +oid sha256:254b7f753b70c964847b686f0f71af751f2f49beea6ede4aeb333fe06062a257 +size 2289 diff --git a/selfdrive/assets/icons_mici/experimental_mode.png b/selfdrive/assets/icons_mici/experimental_mode.png index e0138bfd65..75850d08f5 100644 --- a/selfdrive/assets/icons_mici/experimental_mode.png +++ b/selfdrive/assets/icons_mici/experimental_mode.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb42b8d6259238beb26f286dc28fb2dc8d91b00fec1f7a7655296b5769439a15 -size 15690 +oid sha256:01841b602632c66ab14a8e52b874a1623f09641dc2ef0620f4e2d00bb4a913f3 +size 16243 diff --git a/selfdrive/assets/icons_mici/microphone.png b/selfdrive/assets/icons_mici/microphone.png index 9718a6b135..9af8f2f455 100644 --- a/selfdrive/assets/icons_mici/microphone.png +++ b/selfdrive/assets/icons_mici/microphone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17b6fe530598cbad34bcf31d4f21f929b792aacedef51b3ffef1941c86017811 -size 7331 +oid sha256:744dbaa68ee74e300cd46439bad79449c860e1c5c027304b0f382bd5383fba77 +size 6817 diff --git a/selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png b/selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png index 6a8351f6ee..08181ca35f 100644 --- a/selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png +++ b/selfdrive/assets/icons_mici/offroad_alerts/green_wheel.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05f3626e790622a4ad90e982c4aacb612d0785a752339352a3187addf763e2e9 -size 13288 +oid sha256:3b11ee84d48972a2499cb29f01594d77a1a39692f6424a315a3f83262bc16087 +size 13481 diff --git a/selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png b/selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png index 13af475c6d..52e6836d4b 100644 --- a/selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png +++ b/selfdrive/assets/icons_mici/offroad_alerts/orange_warning.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a877882a8dccb884bd35918f9f9b427a724a59e90a638e54f6fd5d0680ad173c -size 12137 +oid sha256:d548405a65ba4d4590c55866612dc6aa0e78d9278fc864ef60fe3e463edf4a68 +size 12169 diff --git a/selfdrive/assets/icons_mici/offroad_alerts/red_warning.png b/selfdrive/assets/icons_mici/offroad_alerts/red_warning.png index 83c3595b29..df608d3518 100644 --- a/selfdrive/assets/icons_mici/offroad_alerts/red_warning.png +++ b/selfdrive/assets/icons_mici/offroad_alerts/red_warning.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba944b208abed9b8b9752adb8017bd29cd2e98c89fb07ee5d0a595185c7564a5 -size 11898 +oid sha256:b6fc63326d34fbe72f6daf104d101ce19e547dbfe134427c067c957a7179df74 +size 12124 diff --git a/selfdrive/assets/icons_mici/onroad/blind_spot_left.png b/selfdrive/assets/icons_mici/onroad/blind_spot_left.png index 5d3b1e5d7b..fdc189b858 100644 --- a/selfdrive/assets/icons_mici/onroad/blind_spot_left.png +++ b/selfdrive/assets/icons_mici/onroad/blind_spot_left.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a23743d21bc8160e013625210654a55634e4ed58e60057b70e08761bac1c3680 -size 40406 +oid sha256:77b20a8c478d982412d556afb3a035b80b4aa9fe7a86aea761af4a42147d9435 +size 45297 diff --git a/selfdrive/assets/icons_mici/onroad/blind_spot_right.png b/selfdrive/assets/icons_mici/onroad/blind_spot_right.png index 67216078d9..b6cd7834ef 100644 --- a/selfdrive/assets/icons_mici/onroad/blind_spot_right.png +++ b/selfdrive/assets/icons_mici/onroad/blind_spot_right.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acbfa3e38f0b9f422f5c1335ce20013852df2892b813db176a51918adc83ad58 -size 40979 +oid sha256:584cea202afff6dd20d67ae1a9cd6d2b8cc07598bccb91a8d1bac0142567308e +size 45489 diff --git a/selfdrive/assets/icons_mici/onroad/bookmark.png b/selfdrive/assets/icons_mici/onroad/bookmark.png index 207182276e..305561f509 100644 --- a/selfdrive/assets/icons_mici/onroad/bookmark.png +++ b/selfdrive/assets/icons_mici/onroad/bookmark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0d00d743b01c49c2b739127e9916a229caf8c48346d6d168863b080ddcaa409 -size 11124 +oid sha256:fd91685bf656e828648acf035a4737acb2c4709e8514cf0aa0a10fa470a9bb60 +size 11580 diff --git a/selfdrive/assets/icons_mici/onroad/bookmark_fill.png b/selfdrive/assets/icons_mici/onroad/bookmark_fill.png new file mode 100644 index 0000000000..531d5db1cf --- /dev/null +++ b/selfdrive/assets/icons_mici/onroad/bookmark_fill.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3f57346a1cf9a66f9fd746f87bcebb23b7a403e9d6e4fd7701b126abcdd47ea +size 18476 diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png index 04ffc24356..4129b13d92 100644 --- a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png +++ b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7eb870d01e5bf6c421e204026a4ea08e177731f2d6b5b17c4ad43c90c1c3e78 -size 23549 +oid sha256:cb89d9f11cf44992f92142aa5ad84e1ac700a2601aff2abab373e2a822af149e +size 11678 diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png index 540b2029a0..5b917f3a4a 100644 --- a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png +++ b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7b3bb76ee2359076339285ea6bced5b680e5b919a1b7dee163f36cd819c9ea1 -size 1746 +oid sha256:e2772c6a9fe9c57099d347ad49f0cb7c906593f1fdf0e6dde96d104baf0200b0 +size 1365 diff --git a/selfdrive/assets/icons_mici/onroad/eye_fill.png b/selfdrive/assets/icons_mici/onroad/eye_fill.png index 8f0e8ebfb1..78758a9809 100644 --- a/selfdrive/assets/icons_mici/onroad/eye_fill.png +++ b/selfdrive/assets/icons_mici/onroad/eye_fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51af75afbaf30abeaae1c99c7ad3e25cf5d5c90a2d6c799aad353b3302384b0a -size 4829 +oid sha256:07310879d093108435c0011846ae1184966db86443bc6e7ca036a6fa6123700b +size 4983 diff --git a/selfdrive/assets/icons_mici/onroad/eye_orange.png b/selfdrive/assets/icons_mici/onroad/eye_orange.png index b61b9b063c..932c71260b 100644 --- a/selfdrive/assets/icons_mici/onroad/eye_orange.png +++ b/selfdrive/assets/icons_mici/onroad/eye_orange.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88b2ecf3a9834d2b156bb632ec2090d7dc112e8ab61711ba645c03489d1c457f -size 29157 +oid sha256:7be447e56d649e0362ef650494b484e140a01ead31799ce43b266f5781c918d2 +size 36473 diff --git a/selfdrive/assets/icons_mici/onroad/glasses.png b/selfdrive/assets/icons_mici/onroad/glasses.png index 1ac4442f49..006972fd39 100644 --- a/selfdrive/assets/icons_mici/onroad/glasses.png +++ b/selfdrive/assets/icons_mici/onroad/glasses.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28c95c8970648d40b35b94724936a9ab7a6f4cbca367a40f01b86f9abedc70e5 -size 1587 +oid sha256:56de402482b5987ed9a0ff3f793a1c89f857304b34fbb8a3deb5b5d4a332be1c +size 3688 diff --git a/selfdrive/assets/icons_mici/onroad/onroad_fade.png b/selfdrive/assets/icons_mici/onroad/onroad_fade.png index bc12e57e17..3f823061b9 100644 --- a/selfdrive/assets/icons_mici/onroad/onroad_fade.png +++ b/selfdrive/assets/icons_mici/onroad/onroad_fade.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2a2cb4db429467783d7f721ffbed7838551e4aabf32771e73759c87b4a67bca -size 28880 +oid sha256:2aa6d04ba038f15a92868de6e6c7b04f624b4fe89d03bc3e9c4cd44cb729b24e +size 38317 diff --git a/selfdrive/assets/icons_mici/onroad/turn_signal_left.png b/selfdrive/assets/icons_mici/onroad/turn_signal_left.png index 48f52ff9ce..97b5cf1443 100644 --- a/selfdrive/assets/icons_mici/onroad/turn_signal_left.png +++ b/selfdrive/assets/icons_mici/onroad/turn_signal_left.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e845a211cf5d03f781efdd6eec4f8106e8dd85799ea59b51834a9099b479141 -size 30348 +oid sha256:f9f7d0554c0c79ab605c1119ffdef0a4f55196e53b75a65b6ac5218911e24a02 +size 45701 diff --git a/selfdrive/assets/icons_mici/onroad/turn_signal_right.png b/selfdrive/assets/icons_mici/onroad/turn_signal_right.png index 87ca979fbe..6bcb68dac5 100644 --- a/selfdrive/assets/icons_mici/onroad/turn_signal_right.png +++ b/selfdrive/assets/icons_mici/onroad/turn_signal_right.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:009005539f14acc29a4f5510b4e9531d2ba3667133644f6e0069c12b08ba0fd9 -size 35370 +oid sha256:7fae4872ab3c24d5e4c2be6150127a844f89bbdcadfccdff2dfed180e125d577 +size 45699 diff --git a/selfdrive/assets/icons_mici/settings.png b/selfdrive/assets/icons_mici/settings.png index e668ed1fe4..4ba7df9fdf 100644 --- a/selfdrive/assets/icons_mici/settings.png +++ b/selfdrive/assets/icons_mici/settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38a52171bdc6feb3ddfd2d9f9e59db3dabd09fa0aafbc9f81137c59bd03b7c26 -size 2321 +oid sha256:14b457d2dc19d8658f525cc6989c9cfcf0edaf695b18767514242acbdbe2a6dd +size 2198 diff --git a/selfdrive/assets/icons_mici/settings/comma_icon.png b/selfdrive/assets/icons_mici/settings/comma_icon.png index 72a7c8c8f9..dd38a8938f 100644 --- a/selfdrive/assets/icons_mici/settings/comma_icon.png +++ b/selfdrive/assets/icons_mici/settings/comma_icon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10f469a6f5d25d9e2b0b1aae51b4fbd06d2c7b8417613bb321c2a30bb7298dab -size 1392 +oid sha256:7ad4ee47ec6470f788a026f95ed86bf344f64f9cf3186c9c78927233d2694a1d +size 1388 diff --git a/selfdrive/assets/icons_mici/settings/developer/ssh.png b/selfdrive/assets/icons_mici/settings/developer/ssh.png index cd86937aea..0f17d04eca 100644 --- a/selfdrive/assets/icons_mici/settings/developer/ssh.png +++ b/selfdrive/assets/icons_mici/settings/developer/ssh.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c655994336b7da4ca986c6f27494bcab66e77f016ec9db8df271de53ed93e517 -size 1328 +oid sha256:b26133bee089627202d5e89a4e939ad23aaceb5d8e26d7381b1aea3ef892f2ee +size 2620 diff --git a/selfdrive/assets/icons_mici/settings/developer_icon.png b/selfdrive/assets/icons_mici/settings/developer_icon.png index af16c02912..f9d553c7c3 100644 --- a/selfdrive/assets/icons_mici/settings/developer_icon.png +++ b/selfdrive/assets/icons_mici/settings/developer_icon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1f058c5640bd763d2f6927432a1daff1587770ea0d06f2e351a28462e9d8335 -size 1743 +oid sha256:ebb4f7ad9fd2f9fb3c69a38fbc00cbe690809b0ff202ffd4768ae5b699acc035 +size 1759 diff --git a/selfdrive/assets/icons_mici/settings/device/cameras.png b/selfdrive/assets/icons_mici/settings/device/cameras.png index c44c511275..ae9a88c4dc 100644 --- a/selfdrive/assets/icons_mici/settings/device/cameras.png +++ b/selfdrive/assets/icons_mici/settings/device/cameras.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77a1281979f0b50f0e109ead56a88a33b81ef5901dd1a4537eb3fa048e0d90de -size 1345 +oid sha256:5f47e636025e044977f278a35546e0fc971f48fd53c2eeafd3508e95c35f378f +size 3117 diff --git a/selfdrive/assets/icons_mici/settings/device/info.png b/selfdrive/assets/icons_mici/settings/device/info.png index cb16320693..9a29c46d0d 100644 --- a/selfdrive/assets/icons_mici/settings/device/info.png +++ b/selfdrive/assets/icons_mici/settings/device/info.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2649d36259700d32a0edef878647e76492b1bec2fe34ac8ea806d4e7e4c57855 -size 2668 +oid sha256:66858a5d3302333485fa391f7a9bb3a9b1ab4ae881e7fb47b04c3a4507011c94 +size 2613 diff --git a/selfdrive/assets/icons_mici/settings/device/language.png b/selfdrive/assets/icons_mici/settings/device/language.png index f6d57b3134..d2ef27de36 100644 --- a/selfdrive/assets/icons_mici/settings/device/language.png +++ b/selfdrive/assets/icons_mici/settings/device/language.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b982ac1b78b45487490d1dbbffed1f68735f6a35def502e882f706c30683aff -size 3664 +oid sha256:f646263b26de46f79cac836ef6865b0f25ddc91e386b99311723b68bd06693c9 +size 3304 diff --git a/selfdrive/assets/icons_mici/settings/device/lkas.png b/selfdrive/assets/icons_mici/settings/device/lkas.png index 186ea78fb9..80d37d4d5c 100644 --- a/selfdrive/assets/icons_mici/settings/device/lkas.png +++ b/selfdrive/assets/icons_mici/settings/device/lkas.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab6aeb6cba94acf948a0ad64a485db00bf1f3de1360ae4c57212f3f083b2bd24 -size 2554 +oid sha256:a05a41e66c7a24d461a4bbcdab0979031e5900e1db270af52ca363f0bed521f5 +size 2028 diff --git a/selfdrive/assets/icons_mici/settings/device/pair.png b/selfdrive/assets/icons_mici/settings/device/pair.png index f072b2363f..807d44335d 100644 --- a/selfdrive/assets/icons_mici/settings/device/pair.png +++ b/selfdrive/assets/icons_mici/settings/device/pair.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed671f4ad1523f0e66498af39e6075a0c19842ae05eddd00871a6e48ed3685d7 -size 1594 +oid sha256:678483230831d0a7d3dcad5f067a7b641e5d2ae0db477665dfc6c53a675eba18 +size 1779 diff --git a/selfdrive/assets/icons_mici/settings/device/power.png b/selfdrive/assets/icons_mici/settings/device/power.png index a2de14a4e8..711f1a4ab9 100644 --- a/selfdrive/assets/icons_mici/settings/device/power.png +++ b/selfdrive/assets/icons_mici/settings/device/power.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b45645ad9ff27776fdb1caa27827c526cae57f8bd4e23bd1160cb0094121ff2 -size 2338 +oid sha256:a34885e79f42d19b7777dd07e7ab51df344880cb770c48e0baaddb177c2ae938 +size 2228 diff --git a/selfdrive/assets/icons_mici/settings/device/reboot.png b/selfdrive/assets/icons_mici/settings/device/reboot.png index 6c89cd9fc2..298a85c504 100644 --- a/selfdrive/assets/icons_mici/settings/device/reboot.png +++ b/selfdrive/assets/icons_mici/settings/device/reboot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f24039f82d7399d02a155022de65b6dc3b8edcf17059a73a9fd3a9209e3f5575 -size 2360 +oid sha256:1356fe3ddda14568e9be1dca4e16ca9048852e3a27a3f531cd58d7d368485a82 +size 2362 diff --git a/selfdrive/assets/icons_mici/settings/device/uninstall.png b/selfdrive/assets/icons_mici/settings/device/uninstall.png index f9173711eb..53f8bc0e7d 100644 --- a/selfdrive/assets/icons_mici/settings/device/uninstall.png +++ b/selfdrive/assets/icons_mici/settings/device/uninstall.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:558ea538fb258079f9eb05fe048b2806c7635b9f0452af874b00cb8d79b45f9b -size 2421 +oid sha256:50a8ce4fa8ff7f5b0f56ba0dc65b4802dc0be2dc0967b5cb3a15e3b79a4e513e +size 2424 diff --git a/selfdrive/assets/icons_mici/settings/device/up_to_date.png b/selfdrive/assets/icons_mici/settings/device/up_to_date.png index ee925458d3..e09f7d3308 100644 --- a/selfdrive/assets/icons_mici/settings/device/up_to_date.png +++ b/selfdrive/assets/icons_mici/settings/device/up_to_date.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4510e65775c6001758ebcf4dc13e9fa561cce5159d1fd54fbb506f22d3c7bdf3 -size 3149 +oid sha256:61bc44b6e0f99640434d6abcb64880c7bf575eda5cdcf7d74cba7d73307dd39a +size 2739 diff --git a/selfdrive/assets/icons_mici/settings/device/update.png b/selfdrive/assets/icons_mici/settings/device/update.png index cc05931b03..498c066191 100644 --- a/selfdrive/assets/icons_mici/settings/device/update.png +++ b/selfdrive/assets/icons_mici/settings/device/update.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6137349218ea22adba44f46a096afe2efc35536b2251192ed0ea61be443a3c5 -size 2493 +oid sha256:f28cdeaba9146521335bc11ad60a8e0368eb0ed1381e88b35a12a6138ba22ed6 +size 2409 diff --git a/selfdrive/assets/icons_mici/settings/device_icon.png b/selfdrive/assets/icons_mici/settings/device_icon.png index 0caf0d07ce..6a716e4dfd 100644 --- a/selfdrive/assets/icons_mici/settings/device_icon.png +++ b/selfdrive/assets/icons_mici/settings/device_icon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db20bea98259b204be634ce0d9a23fbfdcfc73a324fc0aac0f9ac54e1c51556d -size 2443 +oid sha256:2273629450aa870f0964dd285721c35d3d313fb8b4684122215a65844ae744d0 +size 1888 diff --git a/selfdrive/assets/icons_mici/settings/firehose.png b/selfdrive/assets/icons_mici/settings/firehose.png new file mode 100644 index 0000000000..37451c0482 --- /dev/null +++ b/selfdrive/assets/icons_mici/settings/firehose.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:416656861380981acc114e5285b448d6e4dc42b98539d0ba16821cbc3db89208 +size 1364 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/backspace.png b/selfdrive/assets/icons_mici/settings/keyboard/backspace.png index 342f8e28da..53ff00c2ae 100644 --- a/selfdrive/assets/icons_mici/settings/keyboard/backspace.png +++ b/selfdrive/assets/icons_mici/settings/keyboard/backspace.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:116bbbd1509e6644f7b65b8dacd2402b0918785bd80207504a99ab7e13ab738f -size 2049 +oid sha256:69bb4a401429c3fdf473778f751288b2aafea27eb13f09b20e83d55212f084ba +size 1963 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png b/selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png index d63cc56fbc..2d173bfc9f 100644 --- a/selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png +++ b/selfdrive/assets/icons_mici/settings/keyboard/caps_lock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e8c7fec57640de6bfa8d0ede977e40920a8e651b68ed14e3d6c1850e702f3e3 -size 1399 +oid sha256:563c211fd98018e24418235602e596f3a481f04fddde0a14590e563474fcffd2 +size 1423 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png b/selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png index eb38934302..a3ce71f049 100644 --- a/selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png +++ b/selfdrive/assets/icons_mici/settings/keyboard/caps_lower.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7dab3af28938e9c3ad7b6c3b60526bb76498b0103c7276d90c4bff3622f07d0 -size 1157 +oid sha256:6f81811ea9cdc409d5549035ca928c76e22396193e1cefb6cacab3747ee0c297 +size 1142 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png b/selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png index 4a2cae6c8a..7c147bc07b 100644 --- a/selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png +++ b/selfdrive/assets/icons_mici/settings/keyboard/caps_upper.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c5a88a0e8e810115b6d497d3e230d866bd96a715ddac632f48c78b40e1df702 -size 1059 +oid sha256:60875e73dd9659122c9248d8e99d5cfd301d68dabeec2cb42cebce812c9baae9 +size 1102 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/confirm.png b/selfdrive/assets/icons_mici/settings/keyboard/confirm.png index 09b180e97f..98ca5c61dc 100644 --- a/selfdrive/assets/icons_mici/settings/keyboard/confirm.png +++ b/selfdrive/assets/icons_mici/settings/keyboard/confirm.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32ce109a9fe4814bb9bed88f67d85292791f4a6d7c162e07561920221ac38b2d -size 1411 +oid sha256:43b64365a42d7bf772d567b8867a6ced4ec0175bb88b6acaa3a5345f19ca696e +size 1268 diff --git a/selfdrive/assets/icons_mici/settings/keyboard/space.png b/selfdrive/assets/icons_mici/settings/keyboard/space.png index 778d1847d7..3d61109721 100644 --- a/selfdrive/assets/icons_mici/settings/keyboard/space.png +++ b/selfdrive/assets/icons_mici/settings/keyboard/space.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b04d17f3b0340a94210efa5c9547e0ac340dd6b6dd9ac1f81ba5eb3f89f405d -size 619 +oid sha256:f431e428772991323ee3ce662479e1ab29c3d80a72b93cf9c9673716ba245d5f +size 654 diff --git a/selfdrive/assets/icons_mici/settings/manual_icon.png b/selfdrive/assets/icons_mici/settings/manual_icon.png deleted file mode 100644 index 100b29da45..0000000000 --- a/selfdrive/assets/icons_mici/settings/manual_icon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:957330e9fbc8c03f05dbef8097178a40efc0fc52a6faf7a9917f97046d9a5e99 -size 1559 diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_full.png b/selfdrive/assets/icons_mici/settings/network/cell_strength_full.png index 4bf0cd8726..13f70386d4 100644 --- a/selfdrive/assets/icons_mici/settings/network/cell_strength_full.png +++ b/selfdrive/assets/icons_mici/settings/network/cell_strength_full.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a981d5c5558859b283cb6321c84eec947f82fc2dea8dbdd19b66781e4d3f61f -size 1060 +oid sha256:fb7af523411c5ed75c6e1418dfc2a379486f6dbd7f2f1c281d3ff54e1ea7810e +size 777 diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_high.png b/selfdrive/assets/icons_mici/settings/network/cell_strength_high.png index df6d009335..1fea6d23b8 100644 --- a/selfdrive/assets/icons_mici/settings/network/cell_strength_high.png +++ b/selfdrive/assets/icons_mici/settings/network/cell_strength_high.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58da16ede432cf89096c11dc0f4ea098735863fb09a1d655cb06de8a112bd263 -size 1205 +oid sha256:db86e176e016458fcff00d40e37636a808977e0cc01bcc9c04b31a1001562de8 +size 936 diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_low.png b/selfdrive/assets/icons_mici/settings/network/cell_strength_low.png index c3323a9fea..d763f86c7f 100644 --- a/selfdrive/assets/icons_mici/settings/network/cell_strength_low.png +++ b/selfdrive/assets/icons_mici/settings/network/cell_strength_low.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:031bbd50c34d8fd5e71bdc292ba3e50b28a13c56a48dc84117723f1b35b42f51 -size 1224 +oid sha256:1cd0b3a00db36ee7eacf5887d07d40e5351fb441d98643a02df4c742cd1e935d +size 945 diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png b/selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png index 64ab947c53..148ee63e99 100644 --- a/selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png +++ b/selfdrive/assets/icons_mici/settings/network/cell_strength_medium.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccb5f2227c72dd28e40c9f19965abe007cbd7b47cdca924907dc9fad906f5c81 -size 1219 +oid sha256:25724acfe0c261070b103ef5933053d5dd8b726ece42d0e5f715f05c67be2294 +size 956 diff --git a/selfdrive/assets/icons_mici/settings/network/cell_strength_none.png b/selfdrive/assets/icons_mici/settings/network/cell_strength_none.png index 6cdef706bd..c6d82ac316 100644 --- a/selfdrive/assets/icons_mici/settings/network/cell_strength_none.png +++ b/selfdrive/assets/icons_mici/settings/network/cell_strength_none.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92c195721fe2b4ca42176077bf4ca3484cdfc314e961f1431b2296476bcae891 -size 1178 +oid sha256:cb0aeb6260bcd0642204f842112479f4b19b350db9addae5e14c9c5131bcf956 +size 781 diff --git a/selfdrive/assets/icons_mici/settings/network/new/lock.png b/selfdrive/assets/icons_mici/settings/network/new/lock.png index 0a0b18c7a9..9fc152d3db 100644 --- a/selfdrive/assets/icons_mici/settings/network/new/lock.png +++ b/selfdrive/assets/icons_mici/settings/network/new/lock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40dbbb3000e1137ec11fe658fbfebae7cadfc91356953317335f9bb70fcb40d3 -size 1235 +oid sha256:782161f35b4925c7063c441b0c341331c814614cf241f21b4e70134280c630f0 +size 1182 diff --git a/selfdrive/assets/icons_mici/settings/network/new/trash.png b/selfdrive/assets/icons_mici/settings/network/new/trash.png index 99e1a2e246..81e5f13e43 100644 --- a/selfdrive/assets/icons_mici/settings/network/new/trash.png +++ b/selfdrive/assets/icons_mici/settings/network/new/trash.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efabf98ed66fe4447c0f13c74aec681b084de780c551ce18258c79636d4123c5 -size 1524 +oid sha256:9074162bf0469fc5ab0b5711a121289a983c887161df269ac120edd8fd024499 +size 1533 diff --git a/selfdrive/assets/icons_mici/settings/network/tethering.png b/selfdrive/assets/icons_mici/settings/network/tethering.png index 9e7b90be41..4bb416b0b1 100644 --- a/selfdrive/assets/icons_mici/settings/network/tethering.png +++ b/selfdrive/assets/icons_mici/settings/network/tethering.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2907ce46d1b6e676402f390c530955b65e76baf0b77fafc0616c50b988b3994c -size 1609 +oid sha256:b1e322ea6e57b05b3515fcd4e9100f890e6ff80607c11360b7927fa5a9765beb +size 2752 diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png b/selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png index 1a1655fddc..fe81ffa572 100644 --- a/selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png +++ b/selfdrive/assets/icons_mici/settings/network/wifi_strength_full.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2715ea698eccb3648ab96cbddf897ea1842acbc1eb9667bc6f34aba82d0896b -size 1976 +oid sha256:73c76e5240bdff64c1d1ed0ac2bb9c3fadb2fd61fbf8dc710b812757af8bcf6c +size 2026 diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png b/selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png index 4d64d8062f..2649cc89dc 100644 --- a/selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png +++ b/selfdrive/assets/icons_mici/settings/network/wifi_strength_low.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58d839402c6f002ba8d2217888190b338fc3ac13d372df0988fac7bf95b89302 -size 2111 +oid sha256:e66cc6174a54177793c42ef3525a9aa1592e05b0abb677442c7226269d1371a5 +size 2196 diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png b/selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png index 2d53a20cef..8881833375 100644 --- a/selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png +++ b/selfdrive/assets/icons_mici/settings/network/wifi_strength_medium.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9918724409dbfa1973a097a692c2f57e45cc2bc0ce71c498ef3e02aa82559d3 -size 2128 +oid sha256:7948a9234f2bc996aefb3a9e58a37c06ebbf54e8e4596e47800f78ef7e81961f +size 2231 diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png b/selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png index 482a0e1042..848d7849a2 100644 --- a/selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png +++ b/selfdrive/assets/icons_mici/settings/network/wifi_strength_none.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fcef95eb18e2db566b907ae99b8d8f450424b3b7823fdc24cdfe066ccf64378 -size 2141 +oid sha256:a57ea402448dacc2026631174e448b6254698fe92309221576400cbf28196936 +size 2195 diff --git a/selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png b/selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png index 38ddff84b7..4457a3fcd2 100644 --- a/selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png +++ b/selfdrive/assets/icons_mici/settings/network/wifi_strength_slash.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73e4ae4741a039f41d79827c40be6da83f8c6eb79e9103db2dfec718ca96efb7 -size 2512 +oid sha256:7e6d166bdbbcdc106e7cd4a44ba85848888f18a6ef34e86daac8e12a3f519443 +size 2318 diff --git a/selfdrive/assets/icons_mici/settings/toggles_icon.png b/selfdrive/assets/icons_mici/settings/toggles_icon.png deleted file mode 100644 index ccb343e8ed..0000000000 --- a/selfdrive/assets/icons_mici/settings/toggles_icon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0297535eb73bea71e87c363dc12385bb9163b81403797e50966b20259f725542 -size 2528 diff --git a/selfdrive/assets/icons_mici/setup/back_new.png b/selfdrive/assets/icons_mici/setup/back_new.png index c4834a5649..20e7fe3b88 100644 --- a/selfdrive/assets/icons_mici/setup/back_new.png +++ b/selfdrive/assets/icons_mici/setup/back_new.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7198352d23952d0f2fbc128f20523ea6f2f2b7e378aa495da748a0e34f192806 -size 1641 +oid sha256:d29a9c295b33b3164c37a68ad77795595e6ac877a5b308d28112b0315ecd498f +size 1687 diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png index 92993e3e00..dfb9799b0b 100644 --- a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png +++ b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b7dce550c008ff7a65ed19ccf308ecf92cd0118bb544978b7dd7393c5c27ae5 -size 809 +oid sha256:2290105f9b055b3c3d482d883d148de3418cad07b653133b0f61137e1976c407 +size 1412 diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png index 53a837afbe..fa29be1827 100644 --- a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png +++ b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e102b8b2e71a25d9f818b37d6f75ed958430cb765a07ae50713995779fb6a886 -size 1388 +oid sha256:ec9691d2572e2e084f0b3c99a1dcd0daadf5040d16c02347ffec9dd5466c061a +size 1438 diff --git a/selfdrive/assets/icons_mici/setup/green_car.png b/selfdrive/assets/icons_mici/setup/green_car.png deleted file mode 100644 index 867cadbbd6..0000000000 --- a/selfdrive/assets/icons_mici/setup/green_car.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce8a34777e0b185f457b98845aa17fe6b5192ca46101463aecd21a9e04c0f0f0 -size 13281 diff --git a/selfdrive/assets/icons_mici/setup/green_dm.png b/selfdrive/assets/icons_mici/setup/green_dm.png index d41edd4c2a..87f4ffe788 100644 --- a/selfdrive/assets/icons_mici/setup/green_dm.png +++ b/selfdrive/assets/icons_mici/setup/green_dm.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78795eaa5e0be5fa369e172c02f5bd4b06d20f44363ccb8cbd02cb181b13e529 -size 14289 +oid sha256:8b6d7747dd6bbf47d9782fc0d847c224b933f6616218ade1f9220018aa9d6acc +size 15052 diff --git a/selfdrive/assets/icons_mici/setup/green_info.png b/selfdrive/assets/icons_mici/setup/green_info.png index 309e56e6ee..57e005abd6 100644 --- a/selfdrive/assets/icons_mici/setup/green_info.png +++ b/selfdrive/assets/icons_mici/setup/green_info.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b0b1777d5bed7149982af9f2abab3fab7b6c576e3d53cf2c459804c6ec9ca1e -size 3957 +oid sha256:5055bc385a1de674e6f3cbafdb611ee4b1088de2a3c357bce76f6a192226c952 +size 14154 diff --git a/selfdrive/assets/icons_mici/setup/green_pedal.png b/selfdrive/assets/icons_mici/setup/green_pedal.png deleted file mode 100644 index 2dd18f489a..0000000000 --- a/selfdrive/assets/icons_mici/setup/green_pedal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6cadcda59bc861a1e710e0a8ac67024bdcc44b5f9261abbf098ff11cefb1da51 -size 12209 diff --git a/selfdrive/assets/icons_mici/setup/orange_dm.png b/selfdrive/assets/icons_mici/setup/orange_dm.png index 74cce9d975..97df767a98 100644 --- a/selfdrive/assets/icons_mici/setup/orange_dm.png +++ b/selfdrive/assets/icons_mici/setup/orange_dm.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38a108f96f85a154b698693b07f2e4214124b8f2545b7c4490cea0aa998d75fd -size 11855 +oid sha256:9c45ab0b949c1c71651f9f48cf6ff10196d64eb85e042b063e92b1d7ca02dcb5 +size 13155 diff --git a/selfdrive/assets/icons_mici/setup/red_warning.png b/selfdrive/assets/icons_mici/setup/red_warning.png index ed0634079b..387794cf13 100644 --- a/selfdrive/assets/icons_mici/setup/red_warning.png +++ b/selfdrive/assets/icons_mici/setup/red_warning.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:448d3e7214a77b02b32020ddb440ccd8fe72e110493a51cc10901c8242e72ca8 -size 3185 +oid sha256:e8e8bc3c15df7512a81b902e47fb069eff1370c833095d3b25f3866efb815fff +size 11123 diff --git a/selfdrive/assets/icons_mici/setup/restore.png b/selfdrive/assets/icons_mici/setup/restore.png index 6aa6c6b851..5eff924040 100644 --- a/selfdrive/assets/icons_mici/setup/restore.png +++ b/selfdrive/assets/icons_mici/setup/restore.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d6b99696163cac1867d46998af9e53e212b82641b33c93b51276671f400a5ac -size 2962 +oid sha256:1f5ee67cd334d259ac33f932281db36533877009b5769c92d9cff3054fd5627c +size 2942 diff --git a/selfdrive/assets/icons_mici/setup/scroll_down_indicator.png b/selfdrive/assets/icons_mici/setup/scroll_down_indicator.png index 4d74d86075..3cd26e5181 100644 --- a/selfdrive/assets/icons_mici/setup/scroll_down_indicator.png +++ b/selfdrive/assets/icons_mici/setup/scroll_down_indicator.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52535e34e27b0341f7690a72dc16555eeb6e032bc2c2cde0786469852fdf5987 -size 1267 +oid sha256:a733c425113a7f6ff5ec3dc50ef94b5481c0f2d306e33d1485be8ee6b2798532 +size 1136 diff --git a/selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png b/selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png index bbf1d96254..acf5b17414 100644 --- a/selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png +++ b/selfdrive/assets/icons_mici/setup/small_slider/slider_arrow.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8425c56cb413ba757c94febe0332ce472dbf1472236b03cc4e627746fb86d701 -size 1149 +oid sha256:75a6557935075a646b17d083202832daafb263d4cfa38aea2af407afc04e2ef4 +size 1312 diff --git a/selfdrive/assets/icons_mici/setup/warning.png b/selfdrive/assets/icons_mici/setup/warning.png index 806eea28b7..1b7839f47f 100644 --- a/selfdrive/assets/icons_mici/setup/warning.png +++ b/selfdrive/assets/icons_mici/setup/warning.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bc7a85a0672183d80817f337084060465e143362037955025c11bc8ac531076 -size 3247 +oid sha256:7584d32ac0231381e38646fdac2f71b4517905ef22024f01bd9e124d3918f33a +size 9194 diff --git a/selfdrive/assets/icons_mici/ssh_short.png b/selfdrive/assets/icons_mici/ssh_short.png new file mode 100644 index 0000000000..699ddd72e8 --- /dev/null +++ b/selfdrive/assets/icons_mici/ssh_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef1735e6effcb625ea618fa35a6b908b28ca483d5997e15241d48e2d3d29819e +size 1433 diff --git a/selfdrive/assets/icons_mici/tethering_short.png b/selfdrive/assets/icons_mici/tethering_short.png new file mode 100644 index 0000000000..f97fed95de --- /dev/null +++ b/selfdrive/assets/icons_mici/tethering_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fce940a3cbd2e9530e8efdde90794013a272919b2f3ea482bc06535c795640e7 +size 2176 diff --git a/selfdrive/assets/icons_mici/turn_intent_left.png b/selfdrive/assets/icons_mici/turn_intent_left.png index 6c2c47e882..3934200c9d 100644 --- a/selfdrive/assets/icons_mici/turn_intent_left.png +++ b/selfdrive/assets/icons_mici/turn_intent_left.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ead8287b7041c32456e13721c238a71933256ca3d2b7e649c8f8731585eb5de8 -size 906 +oid sha256:001cb8227eaaff5367055395d9b3ccd5822f9a47276091832d8ad28b074d77c9 +size 914 diff --git a/selfdrive/assets/icons_mici/turn_intent_right.png b/selfdrive/assets/icons_mici/turn_intent_right.png index 03a7245e76..e342778731 100644 --- a/selfdrive/assets/icons_mici/turn_intent_right.png +++ b/selfdrive/assets/icons_mici/turn_intent_right.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fe0532f7040aae78baa85c4cca44f5c939adb6a6f15889e2ca036f4a493f848 -size 935 +oid sha256:7b7e0194a8b9009e493cdce35cd15711596a54227c740e9d6419a3891c6c4037 +size 912 diff --git a/selfdrive/assets/icons_mici/wheel.png b/selfdrive/assets/icons_mici/wheel.png index f122349b82..a43bcb3b99 100644 --- a/selfdrive/assets/icons_mici/wheel.png +++ b/selfdrive/assets/icons_mici/wheel.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc3ef0c8c3038d75f99df2c565a361107bc903944d1afe91de0cbed9f6ca062a -size 2725 +oid sha256:8cf9c6361ed82551eb99e028e0a75ff56b72ca856ccf7c9a76afe6745434980a +size 2720 diff --git a/selfdrive/assets/icons_mici/wheel_critical.png b/selfdrive/assets/icons_mici/wheel_critical.png index c0e5e8619e..676b0b4d71 100644 --- a/selfdrive/assets/icons_mici/wheel_critical.png +++ b/selfdrive/assets/icons_mici/wheel_critical.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12783dc05ea6dae2647ac3a3a7c8391d520c3f0cf2f458333a357ee9633eb6c4 -size 10909 +oid sha256:4c3d9082b295f9e5ddef93f8d4e9cb961ea2374c7affd26394bbccb26e7137b2 +size 11023 diff --git a/selfdrive/car/tests/big_cars_test.sh b/selfdrive/car/tests/big_cars_test.sh index 863b8bead0..bb6e82dd0e 100755 --- a/selfdrive/car/tests/big_cars_test.sh +++ b/selfdrive/car/tests/big_cars_test.sh @@ -6,7 +6,6 @@ cd $BASEDIR export MAX_EXAMPLES=300 export INTERNAL_SEG_CNT=300 -export FILEREADER_CACHE=1 export INTERNAL_SEG_LIST=selfdrive/car/tests/test_models_segs.txt cd selfdrive/car/tests && pytest test_models.py test_car_interfaces.py diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 48d174bfa0..d0eb40d096 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -67,34 +67,35 @@ class LatControlTorque(LatControl): pid_log = log.ControlsState.LateralTorqueState.new_message() pid_log.version = VERSION + measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) + measurement = measured_curvature * CS.vEgo ** 2 + future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2 + self.lat_accel_request_buffer.append(future_desired_lateral_accel) + + roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY + curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) + lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 + + delay_frames = int(np.clip(lat_delay / self.dt + 1, 1, self.lat_accel_request_buffer_len)) + expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames] + setpoint = expected_lateral_accel + error = setpoint - measurement + + lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2)) + raw_lateral_jerk = (self.lat_accel_request_buffer[lookahead_idx+1] - self.lat_accel_request_buffer[lookahead_idx-1]) / (2 * self.dt) + desired_lateral_jerk = self.jerk_filter.update(raw_lateral_jerk) + gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation + ff = gravity_adjusted_future_lateral_accel + # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll + ff -= self.torque_params.latAccelOffset + ff += get_friction(error + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) + if not active: output_torque = 0.0 pid_log.active = False else: - measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) - roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY - curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) - lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 - - delay_frames = int(np.clip(lat_delay / self.dt, 1, self.lat_accel_request_buffer_len)) - expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames] - lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2)) - raw_lateral_jerk = (self.lat_accel_request_buffer[lookahead_idx+1] - self.lat_accel_request_buffer[lookahead_idx-1]) / (2 * self.dt) - desired_lateral_jerk = self.jerk_filter.update(raw_lateral_jerk) - future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2 - self.lat_accel_request_buffer.append(future_desired_lateral_accel) - gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation - setpoint = expected_lateral_accel - - measurement = measured_curvature * CS.vEgo ** 2 - error = setpoint - measurement - # do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly pid_log.error = float(error) - ff = gravity_adjusted_future_lateral_accel - # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll - ff -= self.torque_params.latAccelOffset - ff += get_friction(error + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 output_lataccel = self.pid.update(pid_log.error, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator) diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript b/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript index 164b965142..7a6c02a538 100644 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript +++ b/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript @@ -1,4 +1,4 @@ -Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'pandad_python', 'np_version') +Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'np_version') gen = "c_generated_code" @@ -67,7 +67,7 @@ lenv.Clean(generated_files, Dir(gen)) generated_long = lenv.Command(generated_files, source_list, f"cd {Dir('.').abspath} && python3 long_mpc.py") -lenv.Depends(generated_long, [msgq_python, common_python, pandad_python]) +lenv.Depends(generated_long, [msgq_python, common_python]) lenv["CFLAGS"].append("-DACADOS_WITH_QPOASES") lenv["CXXFLAGS"].append("-DACADOS_WITH_QPOASES") diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index 3f9d8245bd..9408132c5b 100755 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -35,7 +35,7 @@ X_EGO_OBSTACLE_COST = 3. X_EGO_COST = 0. V_EGO_COST = 0. A_EGO_COST = 0. -J_EGO_COST = 5.0 +J_EGO_COST = 5. A_CHANGE_COST = 200. DANGER_ZONE_COST = 100. CRASH_DISTANCE = .25 @@ -43,7 +43,6 @@ LEAD_DANGER_FACTOR = 0.75 LIMIT_COST = 1e6 ACADOS_SOLVER_TYPE = 'SQP_RTI' - # Fewer timestamps don't hurt performance and lead to # much better convergence of the MPC with low iterations N = 12 @@ -57,6 +56,7 @@ COMFORT_BRAKE = 2.5 STOP_DISTANCE = 6.0 CRUISE_MIN_ACCEL = -1.2 CRUISE_MAX_ACCEL = 1.6 +MIN_X_LEAD_FACTOR = 0.5 def get_jerk_factor(personality=log.LongitudinalPersonality.standard): if personality==log.LongitudinalPersonality.relaxed: @@ -85,20 +85,12 @@ def get_stopped_equivalence_factor(v_lead): def get_safe_obstacle_distance(v_ego, t_follow): return (v_ego**2) / (2 * COMFORT_BRAKE) + t_follow * v_ego + STOP_DISTANCE -def desired_follow_distance(v_ego, v_lead, t_follow=None): - if t_follow is None: - t_follow = get_T_FOLLOW() - return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead) - - def gen_long_model(): model = AcadosModel() model.name = MODEL_NAME - # set up states & controls - x_ego = SX.sym('x_ego') - v_ego = SX.sym('v_ego') - a_ego = SX.sym('a_ego') + # states + x_ego, v_ego, a_ego = SX.sym('x_ego'), SX.sym('v_ego'), SX.sym('a_ego') model.x = vertcat(x_ego, v_ego, a_ego) # controls @@ -126,7 +118,6 @@ def gen_long_model(): model.f_expl_expr = f_expl return model - def gen_long_ocp(): ocp = AcadosOcp() ocp.model = gen_long_model() @@ -222,30 +213,31 @@ def gen_long_ocp(): class LongitudinalMpc: - def __init__(self, mode='acc', dt=DT_MDL): - self.mode = mode + def __init__(self, dt=DT_MDL): self.dt = dt self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N) self.reset() self.source = SOURCES[2] def reset(self): - # self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N) self.solver.reset() - # self.solver.options_set('print_level', 2) + + self.x_sol = np.zeros((N+1, X_DIM)) + self.u_sol = np.zeros((N, 1)) self.v_solution = np.zeros(N+1) self.a_solution = np.zeros(N+1) - self.prev_a = np.array(self.a_solution) self.j_solution = np.zeros(N) + self.prev_a = np.array(self.a_solution) self.yref = np.zeros((N+1, COST_DIM)) + for i in range(N): self.solver.cost_set(i, "yref", self.yref[i]) self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM]) - self.x_sol = np.zeros((N+1, X_DIM)) - self.u_sol = np.zeros((N,1)) + self.params = np.zeros((N+1, PARAM_DIM)) for i in range(N+1): self.solver.set(i, 'x', np.zeros(X_DIM)) + self.last_cloudlog_t = 0 self.status = False self.crash_cnt = 0.0 @@ -276,16 +268,9 @@ class LongitudinalMpc: def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard): jerk_factor = get_jerk_factor(personality) - if self.mode == 'acc': - a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0 - cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, jerk_factor * a_change_cost, jerk_factor * J_EGO_COST] - constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST] - elif self.mode == 'blended': - a_change_cost = 40.0 if prev_accel_constraint else 0 - cost_weights = [0., 0.1, 0.2, 5.0, a_change_cost, 1.0] - constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST] - else: - raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner cost set') + a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0 + cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, jerk_factor * a_change_cost, jerk_factor * J_EGO_COST] + constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST] self.set_cost_weights(cost_weights, constraint_cost_weights) def set_cur_state(self, v, a): @@ -320,14 +305,14 @@ class LongitudinalMpc: # MPC will not converge if immediate crash is expected # Clip lead distance to what is still possible to brake for - min_x_lead = ((v_ego + v_lead)/2) * (v_ego - v_lead) / (-ACCEL_MIN * 2) + min_x_lead = MIN_X_LEAD_FACTOR * (v_ego + v_lead) * (v_ego - v_lead) / (-ACCEL_MIN * 2) x_lead = np.clip(x_lead, min_x_lead, 1e8) v_lead = np.clip(v_lead, 0.0, 1e8) a_lead = np.clip(a_lead, -10., 5.) lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau) return lead_xv - def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalPersonality.standard): + def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard): t_follow = get_T_FOLLOW(personality) v_ego = self.x0[1] self.status = radarstate.leadOne.status or radarstate.leadTwo.status @@ -341,56 +326,28 @@ class LongitudinalMpc: lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1]) lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1]) - self.params[:,0] = ACCEL_MIN - self.params[:,1] = ACCEL_MAX + # Fake an obstacle for cruise, this ensures smooth acceleration to set speed + # when the leads are no factor. + v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05) + # TODO does this make sense when max_a is negative? + v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05) + v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper) + cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow) - # Update in ACC mode or ACC/e2e blend - if self.mode == 'acc': - self.params[:,5] = LEAD_DANGER_FACTOR + x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle]) + self.source = SOURCES[np.argmin(x_obstacles[0])] - # Fake an obstacle for cruise, this ensures smooth acceleration to set speed - # when the leads are no factor. - v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05) - # TODO does this make sense when max_a is negative? - v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05) - v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), - v_lower, - v_upper) - cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow) - x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle]) - self.source = SOURCES[np.argmin(x_obstacles[0])] - - # These are not used in ACC mode - x[:], v[:], a[:], j[:] = 0.0, 0.0, 0.0, 0.0 - - elif self.mode == 'blended': - self.params[:,5] = 1.0 - - x_obstacles = np.column_stack([lead_0_obstacle, - lead_1_obstacle]) - cruise_target = T_IDXS * np.clip(v_cruise, v_ego - 2.0, 1e3) + x[0] - xforward = ((v[1:] + v[:-1]) / 2) * (T_IDXS[1:] - T_IDXS[:-1]) - x = np.cumsum(np.insert(xforward, 0, x[0])) - - x_and_cruise = np.column_stack([x, cruise_target]) - x = np.min(x_and_cruise, axis=1) - - self.source = 'e2e' if x_and_cruise[1,0] < x_and_cruise[1,1] else 'cruise' - - else: - raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner update') - - self.yref[:,1] = x - self.yref[:,2] = v - self.yref[:,3] = a - self.yref[:,5] = j + self.yref[:,:] = 0.0 for i in range(N): self.solver.set(i, "yref", self.yref[i]) self.solver.set(N, "yref", self.yref[N][:COST_E_DIM]) + self.params[:,0] = ACCEL_MIN + self.params[:,1] = ACCEL_MAX self.params[:,2] = np.min(x_obstacles, axis=1) self.params[:,3] = np.copy(self.prev_a) self.params[:,4] = t_follow + self.params[:,5] = LEAD_DANGER_FACTOR self.run() if (np.any(lead_xv_0[FCW_IDXS,0] - self.x_sol[FCW_IDXS,0] < CRASH_DISTANCE) and @@ -399,18 +356,7 @@ class LongitudinalMpc: else: self.crash_cnt = 0 - # Check if it got within lead comfort range - # TODO This should be done cleaner - if self.mode == 'blended': - if any((lead_0_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0): - self.source = 'lead0' - if any((lead_1_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0) and \ - (lead_1_obstacle[0] - lead_0_obstacle[0]): - self.source = 'lead1' - def run(self): - # t0 = time.monotonic() - # reset = 0 for i in range(N+1): self.solver.set(i, 'p', self.params[i]) self.solver.constraints_set(0, "lbx", self.x0) @@ -422,13 +368,6 @@ class LongitudinalMpc: self.time_linearization = float(self.solver.get_stats('time_lin')[0]) self.time_integrator = float(self.solver.get_stats('time_sim')[0]) - # qp_iter = self.solver.get_stats('statistics')[-1][-1] # SQP_RTI specific - # print(f"long_mpc timings: tot {self.solve_time:.2e}, qp {self.time_qp_solution:.2e}, lin {self.time_linearization:.2e}, \ - # integrator {self.time_integrator:.2e}, qp_iter {qp_iter}") - # res = self.solver.get_residuals() - # print(f"long_mpc residuals: {res[0]:.2e}, {res[1]:.2e}, {res[2]:.2e}, {res[3]:.2e}") - # self.solver.print_statistics() - for i in range(N+1): self.x_sol[i] = self.solver.get(i, 'x') for i in range(N): @@ -446,12 +385,8 @@ class LongitudinalMpc: self.last_cloudlog_t = t cloudlog.warning(f"Long mpc reset, solution_status: {self.solution_status}") self.reset() - # reset = 1 - # print(f"long_mpc timings: total internal {self.solve_time:.2e}, external: {(time.monotonic() - t0):.2e} qp {self.time_qp_solution:.2e}, \ - # lin {self.time_linearization:.2e} qp_iter {qp_iter}, reset {reset}") if __name__ == "__main__": ocp = gen_long_ocp() AcadosOcpSolver.generate(ocp, json_file=JSON_FILE) - # AcadosOcpSolver.build(ocp.code_export_directory, with_cython=True) diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 0501f669f1..f5bdd37103 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -9,7 +9,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState -from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc +from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, SOURCES from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET @@ -17,7 +17,6 @@ from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP -LON_MPC_STEP = 0.2 # first step is 0.2s A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6] A_CRUISE_MAX_BP = [0., 10.0, 25., 40.] CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N] @@ -28,14 +27,12 @@ MIN_ALLOW_THROTTLE_SPEED = 2.5 _A_TOTAL_MAX_V = [1.7, 3.2] _A_TOTAL_MAX_BP = [20., 40.] - def get_max_accel(v_ego): return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS) def get_coast_accel(pitch): return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py - def limit_accel_in_turns(v_ego, angle_steers, a_target, CP): """ This function returns a limited long acceleration allowed, depending on the existing lateral acceleration @@ -54,8 +51,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP): def __init__(self, CP, CP_SP, init_v=0.0, init_a=0.0, dt=DT_MDL): self.CP = CP self.mpc = LongitudinalMpc(dt=dt) - # TODO remove mpc modes when TR released - self.mpc.mode = 'acc' LongitudinalPlannerSP.__init__(self, self.CP, CP_SP, self.mpc) self.fcw = False self.dt = dt @@ -70,7 +65,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP): self.v_desired_trajectory = np.zeros(CONTROL_N) self.a_desired_trajectory = np.zeros(CONTROL_N) self.j_desired_trajectory = np.zeros(CONTROL_N) - self.solverExecutionTime = 0.0 @staticmethod def parse_model(model_msg): @@ -93,14 +87,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): return x, v, a, j, throttle_prob def update(self, sm): - mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc' - if not self.mlsim: - self.mpc.mode = mode LongitudinalPlannerSP.update(self, sm) - if dec_mpc_mode := self.get_mpc_mode(): - mode = dec_mpc_mode - if not self.mlsim: - self.mpc.mode = dec_mpc_mode if len(sm['carControl'].orientationNED) == 3: accel_coast = get_coast_accel(sm['carControl'].orientationNED[1]) @@ -123,12 +110,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP): # No change cost when user is controlling the speed, or when standstill prev_accel_constraint = not (reset_state or sm['carState'].standstill) - if mode == 'acc': - accel_clip = [ACCEL_MIN, get_max_accel(v_ego)] - steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg - accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP) - else: - accel_clip = [ACCEL_MIN, ACCEL_MAX] + accel_clip = [ACCEL_MIN, get_max_accel(v_ego)] + steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg + accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP) if reset_state: self.v_desired_filter.x = v_ego @@ -137,7 +121,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): # Prevent divergence, smooth in current v_ego self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego)) - x, v, a, j, throttle_prob = self.parse_model(sm['modelV2']) + _, _, _, _, throttle_prob = self.parse_model(sm['modelV2']) # Don't clip at low speeds since throttle_prob doesn't account for creep self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED @@ -154,7 +138,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality) self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired) - self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, personality=sm['selfdriveState'].personality) + self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality) self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution) self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution) @@ -176,12 +160,14 @@ class LongitudinalPlanner(LongitudinalPlannerSP): output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_should_stop_e2e = sm['modelV2'].action.shouldStop - if mode == 'acc' or not self.mlsim: + if sm['selfdriveState'].experimentalMode: + output_a_target = min(output_a_target_e2e, output_a_target_mpc) + self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc + if output_a_target < output_a_target_mpc: + self.mpc.source = SOURCES[3] + else: output_a_target = output_a_target_mpc self.output_should_stop = output_should_stop_mpc - else: - output_a_target = min(output_a_target_mpc, output_a_target_e2e) - self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc for idx in range(2): accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05) diff --git a/selfdrive/controls/tests/test_following_distance.py b/selfdrive/controls/tests/test_following_distance.py index 0fd543dd60..8f66d89bf8 100644 --- a/selfdrive/controls/tests/test_following_distance.py +++ b/selfdrive/controls/tests/test_following_distance.py @@ -4,10 +4,15 @@ from parameterized import parameterized_class from cereal import log -from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import desired_follow_distance, get_T_FOLLOW +from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import get_safe_obstacle_distance, get_stopped_equivalence_factor, get_T_FOLLOW from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver +def desired_follow_distance(v_ego, v_lead, t_follow=None): + if t_follow is None: + t_follow = get_T_FOLLOW() + return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead) + def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personality=0): man = Maneuver( '', diff --git a/selfdrive/controls/tests/test_latcontrol_torque_buffer.py b/selfdrive/controls/tests/test_latcontrol_torque_buffer.py new file mode 100644 index 0000000000..52810ff55b --- /dev/null +++ b/selfdrive/controls/tests/test_latcontrol_torque_buffer.py @@ -0,0 +1,47 @@ +from parameterized import parameterized + +from cereal import car, log +from opendbc.car.car_helpers import interfaces +from opendbc.car.toyota.values import CAR as TOYOTA +from opendbc.car.vehicle_model import VehicleModel +from openpilot.common.realtime import DT_CTRL +from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque, LAT_ACCEL_REQUEST_BUFFER_SECONDS + +from openpilot.selfdrive.car.helpers import convert_to_capnp +from openpilot.selfdrive.locationd.helpers import Pose +from openpilot.common.mock.generators import generate_livePose +from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces + +def get_controller(car_name): + CarInterface = interfaces[car_name] + CP = CarInterface.get_non_essential_params(car_name) + CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) + CI = CarInterface(CP, CP_SP) + sunnypilot_interfaces.setup_interfaces(CI) + CP_SP = convert_to_capnp(CP_SP) + VM = VehicleModel(CP) + controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL) + return controller, VM + +class TestLatControlTorqueBuffer: + + @parameterized.expand([(TOYOTA.TOYOTA_COROLLA_TSS2,)]) + def test_request_buffer_consistency(self, car_name): + buffer_steps = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / DT_CTRL) + controller, VM = get_controller(car_name) + + CS = car.CarState.new_message() + CS.vEgo = 30 + CS.steeringPressed = False + params = log.LiveParametersData.new_message() + + lp = generate_livePose() + pose = Pose.from_live_pose(lp.livePose) + + for _ in range(buffer_steps): + controller.update(True, CS, VM, params, False, 0.001, pose, False, 0.2) + assert all(val != 0 for val in controller.lat_accel_request_buffer) + + for _ in range(buffer_steps): + controller.update(False, CS, VM, params, False, 0.0, pose, False, 0.2) + assert all(val == 0 for val in controller.lat_accel_request_buffer) diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 9bb8b68dc3..903cb5d389 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -29,7 +29,7 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor cython_libs = envCython["LIBS"] + libs commonmodel_lib = lenv.Library('commonmodel', common_src) 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) if 'pycache' not in x] +tinygrad_files = sorted(["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x]) # Get model metadata for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: diff --git a/selfdrive/modeld/models/dmonitoring_model.onnx b/selfdrive/modeld/models/dmonitoring_model.onnx index 9b1c4a1834..4052a15481 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:3446bf8b22e50e47669a25bf32460ae8baf8547037f346753e19ecbfcf6d4e59 -size 6954368 +oid sha256:35e4a5d4c4d481f915e42358af4665b2c92b8f5c1efd1c0731f21b876ad1d856 +size 6954249 diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index 92c81954d2..611ae9fe85 100644 --- a/selfdrive/modeld/models/driving_policy.onnx +++ b/selfdrive/modeld/models/driving_policy.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1edea5bb56f876db4cec97c150799513f6a59373f3ad152d55e4dcaab1b809e3 -size 13926324 +oid sha256:78477124cbf3ffe30fa951ebada8410b43c4242c6054584d656f1d329b067e15 +size 14060847 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index 76c96670a9..6c9fc4c84d 100644 --- a/selfdrive/modeld/models/driving_vision.onnx +++ b/selfdrive/modeld/models/driving_vision.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1dc66bc06f250b577653ccbeaa2c6521b3d46749f601d0a1a366419e929ca438 -size 46271942 +oid sha256:ee29ee5bce84d1ce23e9ff381280de9b4e4d96d2934cd751740354884e112c66 +size 46877473 diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 4f068c4f5a..d6d4566884 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -35,14 +35,7 @@ class DRIVER_MONITOR_SETTINGS: self._EYE_THRESHOLD = 0.65 self._SG_THRESHOLD = 0.9 self._BLINK_THRESHOLD = 0.865 - - self._PHONE_THRESH = 0.75 if device_type == 'mici' else 0.4 - self._PHONE_THRESH2 = 15.0 - self._PHONE_MAX_OFFSET = 0.06 - self._PHONE_MIN_OFFSET = 0.025 - self._PHONE_DATA_AVG = 0.05 - self._PHONE_DATA_VAR = 3*0.005 - self._PHONE_MAX_COUNT = int(360 / self._DT_DMON) + self._PHONE_THRESH = 0.5 self._POSE_PITCH_THRESHOLD = 0.3133 self._POSE_PITCH_THRESHOLD_SLACK = 0.3237 @@ -152,11 +145,10 @@ class DriverMonitoring: # init driver status wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2) - phone_filter_raw_priors = (self.settings._PHONE_DATA_AVG, self.settings._PHONE_DATA_VAR, 2) self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT) - self.phone = DriverProb(raw_priors=phone_filter_raw_priors, max_trackable=self.settings._PHONE_MAX_COUNT) self.pose = DriverPose(settings=self.settings) self.blink = DriverBlink() + self.phone_prob = 0. self.always_on = always_on self.distracted_types = [] @@ -257,12 +249,7 @@ class DriverMonitoring: if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD: distracted_types.append(DistractedType.DISTRACTED_BLINK) - if self.phone.prob_calibrated: - using_phone = self.phone.prob > max(min(self.phone.prob_offseter.filtered_stat.M, self.settings._PHONE_MAX_OFFSET), self.settings._PHONE_MIN_OFFSET) \ - * self.settings._PHONE_THRESH2 - else: - using_phone = self.phone.prob > self.settings._PHONE_THRESH - if using_phone: + if self.phone_prob > self.settings._PHONE_THRESH: distracted_types.append(DistractedType.DISTRACTED_PHONE) return distracted_types @@ -301,7 +288,7 @@ class DriverMonitoring: * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \ * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) - self.phone.prob = driver_data.phoneProb + self.phone_prob = driver_data.phoneProb self.distracted_types = self._get_distracted_types() self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types @@ -315,11 +302,9 @@ class DriverMonitoring: if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted): self.pose.pitch_offseter.push_and_update(self.pose.pitch) self.pose.yaw_offseter.push_and_update(self.pose.yaw) - self.phone.prob_offseter.push_and_update(self.phone.prob) 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.phone.prob_calibrated = self.phone.prob_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT if self.face_detected and not self.driver_distracted: if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD: @@ -425,8 +410,6 @@ class DriverMonitoring: "posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n, "poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(), "poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n, - "phoneProbOffset": self.phone.prob_offseter.filtered_stat.mean(), - "phoneProbValidCount": self.phone.prob_offseter.filtered_stat.n, "stepChange": self.step_change, "awarenessActive": self.awareness_active, "awarenessPassive": self.awareness_passive, diff --git a/selfdrive/pandad/SConscript b/selfdrive/pandad/SConscript index 58777cafe9..5e0b782c1e 100644 --- a/selfdrive/pandad/SConscript +++ b/selfdrive/pandad/SConscript @@ -1,13 +1,9 @@ -Import('env', 'envCython', 'common', 'messaging') +Import('env', 'common', 'messaging') libs = ['usb-1.0', common, messaging, 'pthread'] panda = env.Library('panda', ['panda.cc', 'panda_comms.cc', 'spi.cc']) env.Program('pandad', ['main.cc', 'pandad.cc', 'panda_safety.cc'], LIBS=[panda] + libs) -env.Library('libcan_list_to_can_capnp', ['can_list_to_can_capnp.cc']) - -pandad_python = envCython.Program('pandad_api_impl.so', 'pandad_api_impl.pyx', LIBS=["can_list_to_can_capnp", 'capnp', 'kj'] + envCython["LIBS"]) -Export('pandad_python') if GetOption('extras'): env.Program('tests/test_pandad_usbprotocol', ['tests/test_pandad_usbprotocol.cc'], LIBS=[panda] + libs) diff --git a/selfdrive/pandad/__init__.py b/selfdrive/pandad/__init__.py index cc680e1676..0c17e886a2 100644 --- a/selfdrive/pandad/__init__.py +++ b/selfdrive/pandad/__init__.py @@ -1,4 +1,3 @@ -# Cython, now uses scons to build from openpilot.selfdrive.pandad.pandad_api_impl import can_list_to_can_capnp, can_capnp_to_list assert can_list_to_can_capnp assert can_capnp_to_list diff --git a/selfdrive/pandad/can_list_to_can_capnp.cc b/selfdrive/pandad/can_list_to_can_capnp.cc deleted file mode 100644 index f2cf153453..0000000000 --- a/selfdrive/pandad/can_list_to_can_capnp.cc +++ /dev/null @@ -1,50 +0,0 @@ -#include "cereal/messaging/messaging.h" -#include "selfdrive/pandad/can_types.h" - -void can_list_to_can_capnp_cpp(const std::vector &can_list, std::string &out, bool sendcan, bool valid) { - MessageBuilder msg; - auto event = msg.initEvent(valid); - - auto canData = sendcan ? event.initSendcan(can_list.size()) : event.initCan(can_list.size()); - int j = 0; - for (auto it = can_list.begin(); it != can_list.end(); it++, j++) { - auto c = canData[j]; - c.setAddress(it->address); - c.setDat(kj::arrayPtr((uint8_t*)it->dat.data(), it->dat.size())); - c.setSrc(it->src); - } - const uint64_t msg_size = capnp::computeSerializedSizeInWords(msg) * sizeof(capnp::word); - out.resize(msg_size); - kj::ArrayOutputStream output_stream(kj::ArrayPtr((unsigned char *)out.data(), msg_size)); - capnp::writeMessage(output_stream, msg); -} - -// Converts a vector of Cap'n Proto serialized can strings into a vector of CanData structures. -void can_capnp_to_can_list_cpp(const std::vector &strings, std::vector &can_list, bool sendcan) { - AlignedBuffer aligned_buf; - can_list.reserve(strings.size()); - - for (const auto &str : strings) { - // extract the messages - capnp::FlatArrayMessageReader reader(aligned_buf.align(str.data(), str.size())); - cereal::Event::Reader event = reader.getRoot(); - - auto frames = sendcan ? event.getSendcan() : event.getCan(); - - // Add new CanData entry - CanData &can_data = can_list.emplace_back(); - can_data.nanos = event.getLogMonoTime(); - can_data.frames.reserve(frames.size()); - - // Populate CAN frames - for (const auto &frame : frames) { - CanFrame &can_frame = can_data.frames.emplace_back(); - can_frame.src = frame.getSrc(); - can_frame.address = frame.getAddress(); - - // Copy CAN data - auto dat = frame.getDat(); - can_frame.dat.assign(dat.begin(), dat.end()); - } - } -} diff --git a/selfdrive/pandad/can_types.h b/selfdrive/pandad/can_types.h deleted file mode 100644 index 5fae581cfa..0000000000 --- a/selfdrive/pandad/can_types.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include -#include - -struct CanFrame { - long src; - uint32_t address; - std::vector dat; -}; - -struct CanData { - uint64_t nanos; - std::vector frames; -}; \ No newline at end of file diff --git a/selfdrive/pandad/pandad_api_impl.py b/selfdrive/pandad/pandad_api_impl.py new file mode 100644 index 0000000000..75a7ba484e --- /dev/null +++ b/selfdrive/pandad/pandad_api_impl.py @@ -0,0 +1,88 @@ +import time +from cereal import log + +NO_TRAVERSAL_LIMIT = 2**64 - 1 + +# Cache schema fields for faster access (avoids string lookup on each field access) +_cached_reader_fields = None # (address_field, dat_field, src_field) for reading +_cached_writer_fields = None # (address_field, dat_field, src_field) for writing + + +def _get_reader_fields(schema): + """Get cached schema field objects for reading.""" + global _cached_reader_fields + if _cached_reader_fields is None: + fields = schema.fields + _cached_reader_fields = (fields['address'], fields['dat'], fields['src']) + return _cached_reader_fields + + +def _get_writer_fields(schema): + """Get cached schema field objects for writing.""" + global _cached_writer_fields + if _cached_writer_fields is None: + fields = schema.fields + _cached_writer_fields = (fields['address'], fields['dat'], fields['src']) + return _cached_writer_fields + + +def can_list_to_can_capnp(can_msgs, msgtype='can', valid=True): + """Convert list of CAN messages to Cap'n Proto serialized bytes. + + Args: + can_msgs: List of tuples [(address, data_bytes, src), ...] + msgtype: 'can' or 'sendcan' + valid: Whether the event is valid + + Returns: + Cap'n Proto serialized bytes + """ + global _cached_writer_fields + + dat = log.Event.new_message(valid=valid, logMonoTime=int(time.monotonic() * 1e9)) + can_data = dat.init(msgtype, len(can_msgs)) + + # Cache schema fields on first call + if _cached_writer_fields is None and len(can_msgs) > 0: + _cached_writer_fields = _get_writer_fields(can_data[0].schema) + + if _cached_writer_fields is not None: + addr_f, dat_f, src_f = _cached_writer_fields + for i, msg in enumerate(can_msgs): + f = can_data[i] + f._set_by_field(addr_f, msg[0]) + f._set_by_field(dat_f, msg[1]) + f._set_by_field(src_f, msg[2]) + + return dat.to_bytes() + + +def can_capnp_to_list(strings, msgtype='can'): + """Convert Cap'n Proto serialized bytes to list of CAN messages. + + Args: + strings: Tuple/list of serialized Cap'n Proto bytes + msgtype: 'can' or 'sendcan' + + Returns: + List of tuples [(nanos, [(address, data, src), ...]), ...] + """ + global _cached_reader_fields + result = [] + + for s in strings: + with log.Event.from_bytes(s, traversal_limit_in_words=NO_TRAVERSAL_LIMIT) as event: + frames = getattr(event, msgtype) + + # Cache schema fields on first frame for faster access + if _cached_reader_fields is None and len(frames) > 0: + _cached_reader_fields = _get_reader_fields(frames[0].schema) + + if _cached_reader_fields is not None: + addr_f, dat_f, src_f = _cached_reader_fields + frame_list = [(f._get_by_field(addr_f), f._get_by_field(dat_f), f._get_by_field(src_f)) for f in frames] + else: + frame_list = [] + + result.append((event.logMonoTime, frame_list)) + return result diff --git a/selfdrive/pandad/pandad_api_impl.pyx b/selfdrive/pandad/pandad_api_impl.pyx deleted file mode 100644 index aaecb8a594..0000000000 --- a/selfdrive/pandad/pandad_api_impl.pyx +++ /dev/null @@ -1,56 +0,0 @@ -# distutils: language = c++ -# cython: language_level=3 -from cython.operator cimport dereference as deref, preincrement as preinc -from libcpp.vector cimport vector -from libcpp.string cimport string -from libcpp cimport bool -from libc.stdint cimport uint8_t, uint32_t, uint64_t - -cdef extern from "selfdrive/pandad/can_types.h": - cdef struct CanFrame: - long src - uint32_t address - vector[uint8_t] dat - - cdef struct CanData: - uint64_t nanos - vector[CanFrame] frames - -cdef extern from "can_list_to_can_capnp.cc": - void can_list_to_can_capnp_cpp(const vector[CanFrame] &can_list, string &out, bool sendcan, bool valid) nogil - void can_capnp_to_can_list_cpp(const vector[string] &strings, vector[CanData] &can_data, bool sendcan) - -def can_list_to_can_capnp(can_msgs, msgtype='can', valid=True): - cdef CanFrame *f - cdef vector[CanFrame] can_list - cdef uint32_t cpp_can_msgs_len = len(can_msgs) - - with nogil: - can_list.reserve(cpp_can_msgs_len) - - for can_msg in can_msgs: - f = &(can_list.emplace_back()) - f.address = can_msg[0] - f.dat = can_msg[1] - f.src = can_msg[2] - - cdef string out - cdef bool is_sendcan = (msgtype == 'sendcan') - cdef bool is_valid = valid - with nogil: - can_list_to_can_capnp_cpp(can_list, out, is_sendcan, is_valid) - return out - -def can_capnp_to_list(strings, msgtype='can'): - cdef vector[CanData] data - can_capnp_to_can_list_cpp(strings, data, msgtype == 'sendcan') - - result = [] - cdef CanData *d - cdef vector[CanData].iterator it = data.begin() - while it != data.end(): - d = &deref(it) - frames = [(f.address, (&f.dat[0])[:f.dat.size()], f.src) for f in d.frames] - result.append((d.nanos, frames)) - preinc(it) - return result diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 7e8c553ad3..c92d58c011 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -305,10 +305,10 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { EventName.stockLkas: { ET.PERMANENT: Alert( - "TAKE CONTROL", "Stock LKAS: Lane Departure Detected", - AlertStatus.critical, AlertSize.full, - Priority.HIGH, VisualAlert.fcw, AudibleAlert.none, 2.), + "", + AlertStatus.userPrompt, AlertSize.small, + Priority.LOW, VisualAlert.ldw, AudibleAlert.prompt, 3.), ET.NO_ENTRY: NoEntryAlert("Stock LKAS: Lane Departure Detected"), }, diff --git a/selfdrive/test/longitudinal_maneuvers/maneuver.py b/selfdrive/test/longitudinal_maneuvers/maneuver.py index dfd5b3e109..ba0379f2d7 100644 --- a/selfdrive/test/longitudinal_maneuvers/maneuver.py +++ b/selfdrive/test/longitudinal_maneuvers/maneuver.py @@ -60,7 +60,8 @@ class Maneuver: log['distance_lead'], log['speed'], speed_lead, - log['acceleration']])) + log['acceleration'], + log['d_rel']])) if d_rel < .4 and (self.only_radar or prob_lead > 0.5): print("Crashed!!!!") diff --git a/selfdrive/test/process_replay/README.md b/selfdrive/test/process_replay/README.md index dc801e4285..8e279c71cd 100644 --- a/selfdrive/test/process_replay/README.md +++ b/selfdrive/test/process_replay/README.md @@ -5,7 +5,7 @@ Process replay is a regression test designed to identify any changes in the outp If the test fails, make sure that you didn't unintentionally change anything. If there are intentional changes, the reference logs will be updated. Use `test_processes.py` to run the test locally. -Use `FILEREADER_CACHE='1' test_processes.py` to cache log files. +Log files are cached by default. Use `DISABLE_FILEREADER_CACHE='1' test_processes.py` to disable caching. Currently the following processes are tested: diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 4a58e321fc..85b79391c3 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -e0ad86508edb61b3eaa1b84662c515d2c3368295 \ No newline at end of file +67f3daf309dc6cbb6844fcbaeb83e6596637e551 \ No newline at end of file diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index bdffea4cfe..0da5445ed2 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -92,22 +92,22 @@ class MiciHomeLayout(Widget): self._settings_txt = gui_app.texture("icons_mici/settings.png", 48, 48) self._experimental_txt = gui_app.texture("icons_mici/experimental_mode.png", 48, 48) - self._mic_txt = gui_app.texture("icons_mici/microphone.png", 48, 48) + self._mic_txt = gui_app.texture("icons_mici/microphone.png", 32, 46) self._net_type = NETWORK_TYPES.get(NetworkType.none) self._net_strength = 0 self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 50, 44) - self._wifi_none_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_none.png", 50, 44) - self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 50, 44) - self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 50, 44) - self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 50, 44) + self._wifi_none_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_none.png", 50, 37) + self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 50, 37) + self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 50, 37) + self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 50, 37) - self._cell_none_txt = gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 55, 35) - self._cell_low_txt = gui_app.texture("icons_mici/settings/network/cell_strength_low.png", 55, 35) - self._cell_medium_txt = gui_app.texture("icons_mici/settings/network/cell_strength_medium.png", 55, 35) - self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 55, 35) - self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 55, 35) + self._cell_none_txt = gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 54, 36) + self._cell_low_txt = gui_app.texture("icons_mici/settings/network/cell_strength_low.png", 54, 36) + self._cell_medium_txt = gui_app.texture("icons_mici/settings/network/cell_strength_medium.png", 54, 36) + self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 54, 36) + self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 54, 36) self._openpilot_label = MiciLabel("sunnypilot", font_size=90, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.AUDIOWIDE) self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN) diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 8479ec5c54..9f9c7b83ab 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -127,9 +127,9 @@ class TrainingGuideDMTutorial(Widget): def __init__(self, continue_callback): super().__init__() - self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 48, 48)) + self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48)) self._back_button.set_click_callback(self._show_bad_face_page) - self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 48, 35)) + self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42)) # Wrap the continue callback to restore settings def wrapped_continue_callback(): diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index 8fc63e8963..b6145e042e 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -3,7 +3,7 @@ from collections.abc import Callable from openpilot.common.time_helpers import system_time_valid from openpilot.system.ui.widgets.scroller import Scroller -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle, BigParamControl +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle, BigParamControl, BigCircleParamControl from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import NavWidget @@ -36,15 +36,15 @@ class DeveloperLayoutMici(NavWidget): return gui_app.set_modal_overlay(dlg) - txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 77, 44) + txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 56, 64) github_username = ui_state.params.get("GithubUsername") or "" self._ssh_keys_btn = BigButton("SSH keys", "Not set" if not github_username else github_username, icon=txt_ssh) self._ssh_keys_btn.set_click_callback(ssh_keys_callback) # adb, ssh, ssh keys, debug mode, joystick debug mode, longitudinal maneuver mode, ip address # ******** Main Scroller ******** - self._adb_toggle = BigParamControl("enable ADB", "AdbEnabled") - self._ssh_toggle = BigParamControl("enable SSH", "SshEnabled") + self._adb_toggle = BigCircleParamControl("icons_mici/adb_short.png", "AdbEnabled", icon_size=(82, 82), icon_offset=(0, 12)) + self._ssh_toggle = BigCircleParamControl("icons_mici/ssh_short.png", "SshEnabled", icon_size=(82, 82), icon_offset=(0, 12)) self._joystick_toggle = BigToggle("joystick debug mode", initial_state=ui_state.params.get_bool("JoystickDebugMode"), toggle_callback=self._on_joystick_debug_mode) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 81f0e870ef..8a083a4a7b 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -119,7 +119,7 @@ class UpdaterState(IntEnum): class PairBigButton(BigButton): def __init__(self): - super().__init__("pair", "connect.comma.ai", "icons_mici/settings/comma_icon.png") + super().__init__("pair", "connect.comma.ai", "icons_mici/settings/comma_icon.png", icon_size=(33, 60)) def _update_state(self): if ui_state.prime_state.is_paired(): @@ -153,8 +153,8 @@ UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond class UpdateOpenpilotBigButton(BigButton): def __init__(self): - self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 64) - self._txt_reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 64) + self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) + self._txt_reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70) self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) super().__init__("update sunnypilot", "", self._txt_update_icon) @@ -291,16 +291,16 @@ class DeviceLayoutMici(NavWidget): def uninstall_openpilot_callback(): ui_state.params.put_bool("DoUninstall", True) - reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png") + reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png", icon_size=(114, 60)) reset_calibration_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_calibration_callback, "reset")) uninstall_openpilot_btn = BigButton("uninstall sunnypilot", "", "icons_mici/settings/device/uninstall.png") uninstall_openpilot_btn.set_click_callback(lambda: _engaged_confirmation_callback(uninstall_openpilot_callback, "uninstall")) - reboot_btn = BigCircleButton("icons_mici/settings/device/reboot.png", red=False) + reboot_btn = BigCircleButton("icons_mici/settings/device/reboot.png", red=False, icon_size=(64, 70)) reboot_btn.set_click_callback(lambda: _engaged_confirmation_callback(reboot_callback, "reboot")) - self._power_off_btn = BigCircleButton("icons_mici/settings/device/power.png", red=True) + self._power_off_btn = BigCircleButton("icons_mici/settings/device/power.png", red=True, icon_size=(64, 66)) self._power_off_btn.set_click_callback(lambda: _engaged_confirmation_callback(power_off_callback, "power off")) self._load_languages() diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 1faf49311a..0d5e527836 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -4,7 +4,7 @@ from collections.abc import Callable from openpilot.system.ui.widgets.scroller import Scroller from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle, BigParamControl +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigCircleToggle from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.prime_state import PrimeType @@ -33,15 +33,14 @@ class NetworkLayoutMici(NavWidget): networks_updated=self._on_network_updated, ) - _tethering_icon = "icons_mici/settings/network/tethering.png" - # ******** Tethering ******** def tethering_toggle_callback(checked: bool): self._tethering_toggle_btn.set_enabled(False) self._network_metered_btn.set_enabled(False) self._wifi_manager.set_tethering_active(checked) - self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback) + self._tethering_toggle_btn = BigCircleToggle("icons_mici/tethering_short.png", toggle_callback=tethering_toggle_callback, + icon_size=(82, 82), icon_offset=(0, 12)) def tethering_password_callback(password: str): if password: @@ -53,7 +52,7 @@ class NetworkLayoutMici(NavWidget): confirm_callback=tethering_password_callback) gui_app.set_modal_overlay(dlg) - txt_tethering = gui_app.texture(_tethering_icon, 64, 53) + txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54) self._tethering_password_btn = BigButton("tethering password", "", txt_tethering) self._tethering_password_btn.set_click_callback(tethering_password_clicked) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 565fef5af3..23b89438dc 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -34,12 +34,12 @@ class LoadingAnimation(Widget): class WifiIcon(Widget): def __init__(self): super().__init__() - self.set_rect(rl.Rectangle(0, 0, 89, 64)) + self.set_rect(rl.Rectangle(0, 0, 86, 64)) - self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 89, 64) - self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 89, 64) - self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 89, 64) - self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 23, 32) + self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 86, 64) + self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 86, 64) + self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 86, 64) + self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 22, 32) self._network: Network | None = None self._scale = 1.0 @@ -169,7 +169,7 @@ class ForgetButton(Widget): self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 100, 100) self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 100, 100) - self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 32, 36) + self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 35, 42) self.set_rect(rl.Rectangle(0, 0, 100 + self.HORIZONTAL_MARGIN * 2, 100)) def _handle_mouse_release(self, mouse_pos: MousePos): diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index a452777748..3917899032 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -36,16 +36,16 @@ class SettingsLayout(NavWidget): self._params = Params() self._current_panel = None # PanelType.DEVICE - toggles_btn = BigButton("toggles", "", "icons_mici/settings/toggles_icon.png") + toggles_btn = BigButton("toggles", "", "icons_mici/settings.png") toggles_btn.set_click_callback(lambda: self._set_current_panel(PanelType.TOGGLES)) - network_btn = BigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png") + network_btn = BigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56)) network_btn.set_click_callback(lambda: self._set_current_panel(PanelType.NETWORK)) - device_btn = BigButton("device", "", "icons_mici/settings/device_icon.png") + device_btn = BigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60)) device_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVICE)) - developer_btn = BigButton("developer", "", "icons_mici/settings/developer_icon.png") + developer_btn = BigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60)) developer_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVELOPER)) - firehose_btn = BigButton("firehose", "", "icons_mici/settings/comma_icon.png") + firehose_btn = BigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62)) firehose_btn.set_click_callback(lambda: self._set_current_panel(PanelType.FIREHOSE)) self._scroller = Scroller([ diff --git a/selfdrive/ui/mici/onroad/alert_renderer.py b/selfdrive/ui/mici/onroad/alert_renderer.py index 5c93e5e423..b7a336a35a 100644 --- a/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/selfdrive/ui/mici/onroad/alert_renderer.py @@ -111,10 +111,10 @@ class AlertRenderer(Widget): self._load_icons() def _load_icons(self): - self._txt_turn_signal_left = gui_app.texture('icons_mici/onroad/turn_signal_left.png', 100, 91) - self._txt_turn_signal_right = gui_app.texture('icons_mici/onroad/turn_signal_right.png', 100, 91) - self._txt_blind_spot_left = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 108, 128) - self._txt_blind_spot_right = gui_app.texture('icons_mici/onroad/blind_spot_right.png', 108, 128) + self._txt_turn_signal_left = gui_app.texture('icons_mici/onroad/turn_signal_left.png', 104, 96) + self._txt_turn_signal_right = gui_app.texture('icons_mici/onroad/turn_signal_right.png', 104, 96) + self._txt_blind_spot_left = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 134, 150) + self._txt_blind_spot_right = gui_app.texture('icons_mici/onroad/blind_spot_right.png', 134, 150) def get_alert(self, sm: messaging.SubMaster) -> Alert | None: """Generate the current alert based on selfdrive state.""" diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 316052fdc1..0113066ede 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -50,6 +50,8 @@ class BookmarkIcon(Widget): super().__init__() self._bookmark_callback = bookmark_callback self._icon = gui_app.texture("icons_mici/onroad/bookmark.png", 180, 180) + self._icon_fill = gui_app.texture("icons_mici/onroad/bookmark_fill.png", 180, 180) + self._active_icon = self._icon self._offset_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps) # State @@ -88,6 +90,7 @@ class BookmarkIcon(Widget): if self._offset_filter.x < 1e-3: self._interacting = False + self._active_icon = self._icon def _handle_mouse_event(self, mouse_event: MouseEvent): if not ui_state.started: @@ -100,6 +103,7 @@ class BookmarkIcon(Widget): self._is_swiping = True self._is_swiping_left = False self._state = BookmarkState.DRAGGING + self._active_icon = self._icon elif mouse_event.left_down and self._is_swiping: self._swipe_current_x = mouse_event.pos.x @@ -116,6 +120,7 @@ class BookmarkIcon(Widget): if swipe_distance > self.PEEK_THRESHOLD: self._state = BookmarkState.TRIGGERED self._triggered_time = rl.get_time() + self._active_icon = self._icon_fill self._bookmark_callback() else: # Otherwise, transition back to hidden @@ -129,8 +134,8 @@ class BookmarkIcon(Widget): """Render the bookmark icon.""" if self._offset_filter.x > 0: icon_x = self.rect.x + self.rect.width - round(self._offset_filter.x) - icon_y = self.rect.y + (self.rect.height - self._icon.height) / 2 # Vertically centered - rl.draw_texture(self._icon, int(icon_x), int(icon_y), rl.WHITE) + icon_y = self.rect.y + (self.rect.height - self._active_icon.height) / 2 # Vertically centered + rl.draw_texture(self._active_icon, int(icon_x), int(icon_y), rl.WHITE) class AugmentedRoadView(CameraView): diff --git a/selfdrive/ui/mici/onroad/hud_renderer.py b/selfdrive/ui/mici/onroad/hud_renderer.py index 44ce12437a..659d13fad7 100644 --- a/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/selfdrive/ui/mici/onroad/hud_renderer.py @@ -49,8 +49,8 @@ class TurnIntent(Widget): self._turn_intent_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) self._turn_intent_rotation_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) - self._txt_turn_intent_left: rl.Texture = gui_app.texture('icons_mici/turn_intent_left.png', 50, 19) - self._txt_turn_intent_right: rl.Texture = gui_app.texture('icons_mici/turn_intent_right.png', 50, 19) + self._txt_turn_intent_left: rl.Texture = gui_app.texture('icons_mici/turn_intent_left.png', 50, 20) + self._txt_turn_intent_right: rl.Texture = gui_app.texture('icons_mici/turn_intent_right.png', 50, 20) def _render(self, _): if self._turn_intent_alpha_filter.x > 1e-2: diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 82310577b0..0b252c21aa 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -16,7 +16,7 @@ except ImportError: SCROLLING_SPEED_PX_S = 50 COMPLICATION_SIZE = 36 -LABEL_COLOR = rl.WHITE +LABEL_COLOR = rl.Color(255, 255, 255, int(255 * 0.9)) LABEL_HORIZONTAL_PADDING = 40 COMPLICATION_GREY = rl.Color(0xAA, 0xAA, 0xAA, 255) PRESSED_SCALE = 1.15 if DO_ZOOM else 1.07 @@ -29,9 +29,10 @@ class ScrollState(Enum): class BigCircleButton(Widget): - def __init__(self, icon: str, red: bool = False): + def __init__(self, icon: str, red: bool = False, icon_size: tuple[int, int] = (64, 53), icon_offset: tuple[int, int] = (0, 0)): super().__init__() self._red = red + self._icon_offset = icon_offset # State self.set_rect(rl.Rectangle(0, 0, 180, 180)) @@ -39,7 +40,7 @@ class BigCircleButton(Widget): self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) # Icons - self._txt_icon = gui_app.texture(icon, 64, 53) + self._txt_icon = gui_app.texture(icon, *icon_size) self._txt_btn_disabled_bg = gui_app.texture("icons_mici/buttons/button_circle_disabled.png", 180, 180) self._txt_btn_bg = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180) @@ -66,13 +67,13 @@ class BigCircleButton(Widget): # draw icon icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35)) - rl.draw_texture(self._txt_icon, int(self._rect.x + (self._rect.width - self._txt_icon.width) / 2), - int(self._rect.y + (self._rect.height - self._txt_icon.height) / 2), icon_color) + rl.draw_texture(self._txt_icon, int(self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0]), + int(self._rect.y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), icon_color) class BigCircleToggle(BigCircleButton): - def __init__(self, icon: str, toggle_callback: Callable | None = None): - super().__init__(icon, False) + def __init__(self, icon: str, toggle_callback: Callable | None = None, icon_size: tuple[int, int] = (64, 53), icon_offset: tuple[int, int] = (0, 0)): + super().__init__(icon, False, icon_size=icon_size, icon_offset=icon_offset) self._toggle_callback = toggle_callback # State @@ -80,7 +81,7 @@ class BigCircleToggle(BigCircleButton): # Icons self._txt_toggle_enabled = gui_app.texture("icons_mici/buttons/toggle_dot_enabled.png", 66, 66) - self._txt_toggle_disabled = gui_app.texture("icons_mici/buttons/toggle_dot_disabled.png", 70, 70) # TODO: why discrepancy? + self._txt_toggle_disabled = gui_app.texture("icons_mici/buttons/toggle_dot_disabled.png", 66, 66) def set_checked(self, checked: bool): self._checked = checked @@ -104,11 +105,12 @@ class BigCircleToggle(BigCircleButton): class BigButton(Widget): """A lightweight stand-in for the Qt BigButton, drawn & updated each frame.""" - def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = ""): + def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", icon_size: tuple[int, int] = (64, 64)): super().__init__() self.set_rect(rl.Rectangle(0, 0, 402, 180)) self.text = text self.value = value + self._icon_size = icon_size self.set_icon(icon) self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) @@ -134,7 +136,7 @@ class BigButton(Widget): self._scroll_state = ScrollState.PRE_SCROLL def set_icon(self, icon: Union[str, rl.Texture]): - self._txt_icon = gui_app.texture(icon, 64, 64) if isinstance(icon, str) and len(icon) else icon + self._txt_icon = gui_app.texture(icon, *self._icon_size) if isinstance(icon, str) and len(icon) else icon def set_rotate_icon(self, rotate: bool): if rotate and self._rotate_icon_t is not None: @@ -361,8 +363,9 @@ class BigParamControl(BigToggle): # TODO: param control base class class BigCircleParamControl(BigCircleToggle): - def __init__(self, icon: str, param: str, toggle_callback: Callable | None = None): - super().__init__(icon, toggle_callback) + def __init__(self, icon: str, param: str, toggle_callback: Callable | None = None, icon_size: tuple[int, int] = (64, 53), + icon_offset: tuple[int, int] = (0, 0)): + super().__init__(icon, toggle_callback, icon_size=icon_size, icon_offset=icon_offset) self._param = param self.params = Params() self.set_checked(self.params.get_bool(self._param, False)) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index abd558aa8d..b23abe6080 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -132,6 +132,7 @@ class BigConfirmationDialogV2(BigDialogBase): class BigInputDialog(BigDialogBase): BACK_TOUCH_AREA_PERCENTAGE = 0.2 BACKSPACE_RATE = 25 # hz + TEXT_INPUT_SIZE = 35 def __init__(self, hint: str, @@ -147,10 +148,10 @@ class BigInputDialog(BigDialogBase): self._backspace_held_time: float | None = None - self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 44, 44) + self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 42, 36) self._backspace_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) - self._enter_img = gui_app.texture("icons_mici/settings/keyboard/confirm.png", 44, 44) + self._enter_img = gui_app.texture("icons_mici/settings/keyboard/confirm.png", 42, 36) self._enter_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) # rects for top buttons @@ -179,53 +180,44 @@ class BigInputDialog(BigDialogBase): self._backspace_held_time = None def _render(self, _): - text_input_size = 35 - # draw current text so far below everything. text floats left but always stays in view text = self._keyboard.text() candidate_char = self._keyboard.get_candidate_character() - text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, text_input_size) - text_x = PADDING * 2 + self._enter_img.width + text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, self.TEXT_INPUT_SIZE) - # text needs to move left if we're at the end where right button is - text_rect = rl.Rectangle(text_x, - int(self._rect.y + PADDING), - # clip width to right button when in view - int(self._rect.width - text_x - PADDING * 2 - self._enter_img.width + 5), # TODO: why 5? - int(text_size.y)) - - # draw rounded background for text input bg_block_margin = 5 - text_field_rect = rl.Rectangle(text_rect.x - bg_block_margin, text_rect.y - bg_block_margin, - text_rect.width + bg_block_margin * 2, text_input_size + bg_block_margin * 2) + text_x = PADDING * 2 + self._enter_img.width + bg_block_margin + text_field_rect = rl.Rectangle(text_x, int(self._rect.y + PADDING) - bg_block_margin, + int(self._rect.width - text_x - PADDING * 2 - self._enter_img.width) - bg_block_margin * 2, + int(text_size.y)) # draw text input # push text left with a gradient on left side if too long - if text_size.x > text_rect.width: - text_x -= text_size.x - text_rect.width + if text_size.x > text_field_rect.width: + text_x -= text_size.x - text_field_rect.width - rl.begin_scissor_mode(int(text_rect.x), int(text_rect.y), int(text_rect.width), int(text_rect.height)) - rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), text, rl.Vector2(text_x, text_rect.y), text_input_size, 0, rl.WHITE) + rl.begin_scissor_mode(int(text_field_rect.x), int(text_field_rect.y), int(text_field_rect.width), int(text_field_rect.height)) + rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), text, rl.Vector2(text_x, text_field_rect.y), self.TEXT_INPUT_SIZE, 0, rl.WHITE) # draw grayed out character user is hovering over if candidate_char: - candidate_char_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), candidate_char, text_input_size) + candidate_char_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), candidate_char, self.TEXT_INPUT_SIZE) rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), candidate_char, - rl.Vector2(min(text_x + text_size.x, text_rect.x + text_rect.width) - candidate_char_size.x, text_rect.y), - text_input_size, 0, rl.Color(255, 255, 255, 128)) + rl.Vector2(min(text_x + text_size.x, text_field_rect.x + text_field_rect.width) - candidate_char_size.x, text_field_rect.y), + self.TEXT_INPUT_SIZE, 0, rl.Color(255, 255, 255, 128)) rl.end_scissor_mode() # draw gradient on left side to indicate more text - if text_size.x > text_rect.width: - rl.draw_rectangle_gradient_h(int(text_rect.x), int(text_rect.y), 80, int(text_rect.height), + if text_size.x > text_field_rect.width: + rl.draw_rectangle_gradient_h(int(text_field_rect.x), int(text_field_rect.y), 80, int(text_field_rect.height), rl.BLACK, rl.BLANK) # draw cursor if text: blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2 - cursor_x = min(text_x + text_size.x + 3, text_rect.x + text_rect.width) - rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_rect.y), 4, int(text_size.y)), + cursor_x = min(text_x + text_size.x + 3, text_field_rect.x + text_field_rect.width) + rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_field_rect.y), 4, int(text_size.y)), 1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha))) # draw backspace icon with nice fade @@ -255,7 +247,6 @@ class BigInputDialog(BigDialogBase): # draw debugging rect bounds if DEBUG: rl.draw_rectangle_lines_ex(text_field_rect, 1, rl.Color(100, 100, 100, 255)) - rl.draw_rectangle_lines_ex(text_rect, 1, rl.Color(0, 255, 0, 255)) rl.draw_rectangle_lines_ex(self._top_right_button_rect, 1, rl.Color(0, 255, 0, 255)) rl.draw_rectangle_lines_ex(self._top_left_button_rect, 1, rl.Color(0, 255, 0, 255)) diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index e064205d59..88bab2d001 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -24,7 +24,7 @@ class PairingDialog(NavWidget): self._qr_texture: rl.Texture | None = None self._last_qr_generation = float("-inf") - self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 84, 64) + self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 33, 60) self._pair_label = MiciLabel("pair with comma connect", 48, font_weight=FontWeight.BOLD, color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=40, wrap_text=True) diff --git a/selfdrive/ui/translations/app_fr.po b/selfdrive/ui/translations/app_fr.po index f883d4d485..409761588e 100644 --- a/selfdrive/ui/translations/app_fr.po +++ b/selfdrive/ui/translations/app_fr.po @@ -5,10 +5,10 @@ # msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-10-23 00:50-0700\n" -"PO-Revision-Date: 2025-10-20 18:19-0700\n" +"PO-Revision-Date: 2026-01-24 12:37+0100\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: fr\n" @@ -16,21 +16,22 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.8\n" #: selfdrive/ui/layouts/settings/device.py:160 #, python-format msgid " Steering torque response calibration is complete." -msgstr "" +msgstr " L'étalonnage de la réponse du couple de direction est terminé." #: selfdrive/ui/layouts/settings/device.py:158 #, python-format msgid " Steering torque response calibration is {}% complete." -msgstr "" +msgstr " L'étalonnage de la réponse du couple de direction est terminé à {}%." #: selfdrive/ui/layouts/settings/device.py:133 #, python-format msgid " Your device is pointed {:.1f}° {} and {:.1f}° {}." -msgstr "" +msgstr " Votre appareil est orienté {:.1f}° {} et {:.1f}° {}." #: selfdrive/ui/layouts/sidebar.py:43 msgid "--" @@ -79,12 +80,13 @@ msgstr "" #: selfdrive/ui/layouts/settings/device.py:148 #, python-format msgid "

Steering lag calibration is complete." -msgstr "" +msgstr "

L'étalonnage du délai de réponse de la direction est terminé." #: selfdrive/ui/layouts/settings/device.py:146 #, python-format msgid "

Steering lag calibration is {}% complete." msgstr "" +"

L'étalonnage du délai de réponse de la direction est terminé à {}%." #: selfdrive/ui/layouts/settings/firehose.py:138 #, python-format @@ -107,7 +109,7 @@ msgstr "AJOUTER" #: system/ui/widgets/network.py:139 #, python-format msgid "APN Setting" -msgstr "" +msgstr "Paramètres APN" #: selfdrive/ui/widgets/offroad_alerts.py:109 #, python-format @@ -117,7 +119,7 @@ msgstr "Accuser réception d'actionnement excessif" #: system/ui/widgets/network.py:74 system/ui/widgets/network.py:95 #, python-format msgid "Advanced" -msgstr "" +msgstr "Avancé" #: selfdrive/ui/layouts/settings/toggles.py:98 #, python-format @@ -208,18 +210,18 @@ msgstr "CONNECTER" #: system/ui/widgets/network.py:369 #, python-format msgid "CONNECTING..." -msgstr "CONNECTER" +msgstr "CONNECTER..." #: system/ui/widgets/confirm_dialog.py:23 system/ui/widgets/option_dialog.py:35 #: system/ui/widgets/keyboard.py:81 system/ui/widgets/network.py:318 #, python-format msgid "Cancel" -msgstr "" +msgstr "Annuler" #: system/ui/widgets/network.py:134 #, python-format msgid "Cellular Metered" -msgstr "" +msgstr "Données cellulaire limitées" #: selfdrive/ui/layouts/settings/device.py:68 #, python-format @@ -230,7 +232,7 @@ msgstr "Changer la langue" #, python-format msgid "Changing this setting will restart openpilot if the car is powered on." msgstr "" -" La modification de ce réglage redémarrera openpilot si la voiture est sous " +"La modification de ce réglage redémarrera openpilot si la voiture est sous " "tension." #: selfdrive/ui/widgets/pairing_dialog.py:129 @@ -318,7 +320,7 @@ msgstr "Personnalité de conduite" #: system/ui/widgets/network.py:123 system/ui/widgets/network.py:139 #, python-format msgid "EDIT" -msgstr "" +msgstr "EDITER" #: selfdrive/ui/layouts/sidebar.py:138 msgid "ERROR" @@ -387,22 +389,22 @@ msgstr "" #: system/ui/widgets/network.py:204 #, python-format msgid "Enter APN" -msgstr "" +msgstr "Saisir l'APN" #: system/ui/widgets/network.py:241 #, python-format msgid "Enter SSID" -msgstr "" +msgstr "Entrer le SSID" #: system/ui/widgets/network.py:254 #, python-format msgid "Enter new tethering password" -msgstr "" +msgstr "Saisir le mot de passe du partage de connexion" #: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 #, python-format msgid "Enter password" -msgstr "" +msgstr "Saisir le mot de passe" #: selfdrive/ui/widgets/ssh_key.py:89 #, python-format @@ -412,7 +414,7 @@ msgstr "Entrez votre nom d'utilisateur GitHub" #: system/ui/widgets/list_view.py:123 system/ui/widgets/list_view.py:160 #, python-format msgid "Error" -msgstr "" +msgstr "Erreur" #: selfdrive/ui/layouts/settings/toggles.py:52 #, python-format @@ -431,7 +433,7 @@ msgstr "" #: system/ui/widgets/network.py:373 #, python-format msgid "FORGETTING..." -msgstr "" +msgstr "OUBLIER..." #: selfdrive/ui/widgets/setup.py:44 #, python-format @@ -493,12 +495,12 @@ msgstr "" #: system/ui/widgets/network.py:318 system/ui/widgets/network.py:451 #, python-format msgid "Forget" -msgstr "" +msgstr "Oublier" #: system/ui/widgets/network.py:319 #, python-format msgid "Forget Wi-Fi Network \"{}\"?" -msgstr "" +msgstr "Oublier le réseau Wi-Fi \"{}\" ?" #: selfdrive/ui/layouts/sidebar.py:71 selfdrive/ui/layouts/sidebar.py:125 msgid "GOOD" @@ -532,7 +534,7 @@ msgstr "INSTALLER" #: system/ui/widgets/network.py:150 #, python-format msgid "IP Address" -msgstr "" +msgstr "Adresse IP" #: selfdrive/ui/layouts/settings/software.py:53 #, python-format @@ -574,7 +576,7 @@ msgstr "" #: selfdrive/ui/layouts/settings/device.py:60 #, python-format msgid "N/A" -msgstr "" +msgstr "NC" #: selfdrive/ui/layouts/sidebar.py:142 msgid "NO" @@ -592,7 +594,7 @@ msgstr "Aucune clé SSH trouvée" #: selfdrive/ui/widgets/ssh_key.py:126 #, python-format msgid "No SSH keys found for user '{}'" -msgstr "Aucune clé SSH trouvée pour l'utilisateur '{username}'" +msgstr "Aucune clé SSH trouvée pour l'utilisateur '{}'" #: selfdrive/ui/widgets/offroad_alerts.py:320 #, python-format @@ -677,11 +679,15 @@ msgstr "Éteindre" #, python-format msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "" +"Eviter les transferts de données volumineux lorsque vous êtes connecté à un " +"réseau Wi-Fi limité" #: system/ui/widgets/network.py:135 #, python-format msgid "Prevent large data uploads when on a metered cellular connection" msgstr "" +"Eviter les transferts de données volumineux lors d'une connexion à un réseau " +"cellulaire limité" #: selfdrive/ui/layouts/settings/device.py:25 msgid "" @@ -802,32 +808,32 @@ msgstr "Consultez les règles, fonctionnalités et limitations d'openpilot" #: selfdrive/ui/layouts/settings/software.py:61 #, python-format msgid "SELECT" -msgstr "" +msgstr "SELECTIONNER" #: selfdrive/ui/layouts/settings/developer.py:53 #, python-format msgid "SSH Keys" -msgstr "" +msgstr "Clefs SSH" #: system/ui/widgets/network.py:310 #, python-format msgid "Scanning Wi-Fi networks..." -msgstr "" +msgstr "Analyse des réseaux Wi-Fi..." #: system/ui/widgets/option_dialog.py:36 #, python-format msgid "Select" -msgstr "" +msgstr "Sélectionner" #: selfdrive/ui/layouts/settings/software.py:183 #, python-format msgid "Select a branch" -msgstr "" +msgstr "Sélectionner une branche" #: selfdrive/ui/layouts/settings/device.py:91 #, python-format msgid "Select a language" -msgstr "" +msgstr "Sélectionner un langage" #: selfdrive/ui/layouts/settings/device.py:60 #, python-format @@ -880,12 +886,12 @@ msgstr "TEMPÉRATURE" #: selfdrive/ui/layouts/settings/software.py:61 #, python-format msgid "Target Branch" -msgstr "" +msgstr "Branche cible" #: system/ui/widgets/network.py:124 #, python-format msgid "Tethering Password" -msgstr "" +msgstr "Mot de passe du partage de connexion" #: selfdrive/ui/layouts/settings/settings.py:64 msgid "Toggles" @@ -986,12 +992,12 @@ msgstr "Wi‑Fi" #: system/ui/widgets/network.py:144 #, python-format msgid "Wi-Fi Network Metered" -msgstr "" +msgstr "Réseau Wi-Fi limité" #: system/ui/widgets/network.py:314 #, python-format msgid "Wrong password" -msgstr "" +msgstr "Mauvais mot de passe" #: selfdrive/ui/layouts/onboarding.py:145 #, python-format @@ -1020,12 +1026,12 @@ msgstr "comma prime" #: system/ui/widgets/network.py:142 #, python-format msgid "default" -msgstr "" +msgstr "défaut" #: selfdrive/ui/layouts/settings/device.py:133 #, python-format msgid "down" -msgstr "" +msgstr "bas" #: selfdrive/ui/layouts/settings/software.py:106 #, python-format @@ -1035,7 +1041,7 @@ msgstr "échec de la vérification de mise à jour" #: system/ui/widgets/network.py:237 system/ui/widgets/network.py:314 #, python-format msgid "for \"{}\"" -msgstr "" +msgstr "pour \"{}\"" #: selfdrive/ui/onroad/hud_renderer.py:177 #, python-format @@ -1045,17 +1051,17 @@ msgstr "km/h" #: system/ui/widgets/network.py:204 #, python-format msgid "leave blank for automatic configuration" -msgstr "" +msgstr "ne pas remplir pour une configuration automatique" #: selfdrive/ui/layouts/settings/device.py:134 #, python-format msgid "left" -msgstr "" +msgstr "gauche" #: system/ui/widgets/network.py:142 #, python-format msgid "metered" -msgstr "" +msgstr "limité" #: selfdrive/ui/onroad/hud_renderer.py:177 #, python-format @@ -1116,7 +1122,7 @@ msgid "" "openpilot is continuously calibrating, resetting is rarely required. " "Resetting calibration will restart openpilot if the car is powered on." msgstr "" -" La modification de ce réglage redémarrera openpilot si la voiture est sous " +"La modification de ce réglage redémarrera openpilot si la voiture est sous " "tension." #: selfdrive/ui/layouts/settings/firehose.py:20 @@ -1153,17 +1159,17 @@ msgstr "" #: selfdrive/ui/layouts/settings/device.py:134 #, python-format msgid "right" -msgstr "" +msgstr "droite" #: system/ui/widgets/network.py:142 #, python-format msgid "unmetered" -msgstr "" +msgstr "non limité" #: selfdrive/ui/layouts/settings/device.py:133 #, python-format msgid "up" -msgstr "" +msgstr "haut" #: selfdrive/ui/layouts/settings/software.py:117 #, python-format diff --git a/sunnypilot/models/tests/model_hash b/sunnypilot/models/tests/model_hash index ab5ff86cef..f363f8309a 100644 --- a/sunnypilot/models/tests/model_hash +++ b/sunnypilot/models/tests/model_hash @@ -1 +1 @@ -0cde93a9d20f35619182e704261ef8f6a849149fe5849ba2d355b0b2c89af53c \ No newline at end of file +32f57bdc91f910df1f48ddae7c59aaf6e751f9df6756da481a210577dbce8bcf \ No newline at end of file diff --git a/system/hardware/tici/amplifier.py b/system/hardware/tici/amplifier.py index d714837bb3..09436e6ff4 100755 --- a/system/hardware/tici/amplifier.py +++ b/system/hardware/tici/amplifier.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 import time -from smbus2 import SMBus from collections import namedtuple +from openpilot.common.i2c import SMBus + # https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf AmpConfig = namedtuple('AmpConfig', ['name', 'value', 'register', 'offset', 'mask']) diff --git a/system/sensord/sensors/i2c_sensor.py b/system/sensord/sensors/i2c_sensor.py index 336ebb1fd3..57edcc52d9 100644 --- a/system/sensord/sensors/i2c_sensor.py +++ b/system/sensord/sensors/i2c_sensor.py @@ -1,9 +1,10 @@ import time -import smbus2 import ctypes from collections.abc import Iterable from cereal import log +from openpilot.common.i2c import SMBus + class Sensor: class SensorException(Exception): @@ -13,7 +14,7 @@ class Sensor: pass def __init__(self, bus: int) -> None: - self.bus = smbus2.SMBus(bus) + self.bus = SMBus(bus) self.source = log.SensorEventData.SensorSource.velodyne # unknown self.start_ts = 0. diff --git a/system/ubloxd/SConscript b/system/ubloxd/SConscript deleted file mode 100644 index 9eb50760ba..0000000000 --- a/system/ubloxd/SConscript +++ /dev/null @@ -1,11 +0,0 @@ -Import('env') - -if GetOption('kaitai'): - current_dir = Dir('./generated/').srcnode().abspath - python_cmd = f"kaitai-struct-compiler --target python --outdir {current_dir} $SOURCES" - env.Command(File('./generated/ubx.py'), 'ubx.ksy', python_cmd) - env.Command(File('./generated/gps.py'), 'gps.ksy', python_cmd) - env.Command(File('./generated/glonass.py'), 'glonass.ksy', python_cmd) - # kaitai issue: https://github.com/kaitai-io/kaitai_struct/issues/910 - py_glonass_fix = env.Command(None, File('./generated/glonass.py'), "sed -i 's/self._io.align_to_byte()/# self._io.align_to_byte()/' $SOURCES") - env.Depends(py_glonass_fix, File('./generated/glonass.py')) diff --git a/system/ubloxd/binary_struct.py b/system/ubloxd/binary_struct.py new file mode 100644 index 0000000000..7b229620a2 --- /dev/null +++ b/system/ubloxd/binary_struct.py @@ -0,0 +1,280 @@ +""" +Binary struct parsing DSL. + +Defines a declarative schema for binary messages using dataclasses +and type annotations. +""" + +import struct +from enum import Enum +from dataclasses import dataclass, is_dataclass +from typing import Annotated, Any, TypeVar, get_args, get_origin + + +class FieldType: + """Base class for field type descriptors.""" + + +@dataclass(frozen=True) +class IntType(FieldType): + bits: int + signed: bool + big_endian: bool = False + +@dataclass(frozen=True) +class FloatType(FieldType): + bits: int + +@dataclass(frozen=True) +class BitsType(FieldType): + bits: int + +@dataclass(frozen=True) +class BytesType(FieldType): + size: int + +@dataclass(frozen=True) +class ArrayType(FieldType): + element_type: Any + count_field: str + +@dataclass(frozen=True) +class SwitchType(FieldType): + selector: str + cases: dict[Any, Any] + default: Any = None + +@dataclass(frozen=True) +class EnumType(FieldType): + base_type: FieldType + enum_cls: type[Enum] + +@dataclass(frozen=True) +class ConstType(FieldType): + base_type: FieldType + expected: Any + +@dataclass(frozen=True) +class SubstreamType(FieldType): + length_field: str + element_type: Any + +# Common types - little endian +u8 = IntType(8, False) +u16 = IntType(16, False) +u32 = IntType(32, False) +s8 = IntType(8, True) +s16 = IntType(16, True) +s32 = IntType(32, True) +f32 = FloatType(32) +f64 = FloatType(64) +# Big endian variants +u16be = IntType(16, False, big_endian=True) +u32be = IntType(32, False, big_endian=True) +s16be = IntType(16, True, big_endian=True) +s32be = IntType(32, True, big_endian=True) + + +def bits(n: int) -> BitsType: + """Create a bit-level field type.""" + return BitsType(n) + +def bytes_field(size: int) -> BytesType: + """Create a fixed-size bytes field.""" + return BytesType(size) + +def array(element_type: Any, count_field: str) -> ArrayType: + """Create an array/repeated field.""" + return ArrayType(element_type, count_field) + +def switch(selector: str, cases: dict[Any, Any], default: Any = None) -> SwitchType: + """Create a switch-on field.""" + return SwitchType(selector, cases, default) + +def enum(base_type: Any, enum_cls: type[Enum]) -> EnumType: + """Create an enum-wrapped field.""" + field_type = _field_type_from_spec(base_type) + if field_type is None: + raise TypeError(f"Unsupported field type: {base_type!r}") + return EnumType(field_type, enum_cls) + +def const(base_type: Any, expected: Any) -> ConstType: + """Create a constant-value field.""" + field_type = _field_type_from_spec(base_type) + if field_type is None: + raise TypeError(f"Unsupported field type: {base_type!r}") + return ConstType(field_type, expected) + +def substream(length_field: str, element_type: Any) -> SubstreamType: + """Parse a fixed-length substream using an inner schema.""" + return SubstreamType(length_field, element_type) + + +class BinaryReader: + def __init__(self, data: bytes): + self.data = data + self.pos = 0 + self.bit_pos = 0 # 0-7, position within current byte + + def _require(self, n: int) -> None: + if self.pos + n > len(self.data): + raise EOFError("Unexpected end of data") + + def _read_struct(self, fmt: str): + self._align_to_byte() + size = struct.calcsize(fmt) + self._require(size) + value = struct.unpack_from(fmt, self.data, self.pos)[0] + self.pos += size + return value + + def read_bytes(self, n: int) -> bytes: + self._align_to_byte() + self._require(n) + result = self.data[self.pos : self.pos + n] + self.pos += n + return result + + def read_bits_int_be(self, n: int) -> int: + result = 0 + bits_remaining = n + while bits_remaining > 0: + if self.pos >= len(self.data): + raise EOFError("Unexpected end of data while reading bits") + bits_in_byte = 8 - self.bit_pos + bits_to_read = min(bits_remaining, bits_in_byte) + byte_val = self.data[self.pos] + shift = bits_in_byte - bits_to_read + mask = (1 << bits_to_read) - 1 + extracted = (byte_val >> shift) & mask + result = (result << bits_to_read) | extracted + self.bit_pos += bits_to_read + bits_remaining -= bits_to_read + if self.bit_pos >= 8: + self.bit_pos = 0 + self.pos += 1 + return result + + def _align_to_byte(self) -> None: + if self.bit_pos > 0: + self.bit_pos = 0 + self.pos += 1 + + +T = TypeVar('T', bound='BinaryStruct') + + +class BinaryStruct: + """Base class for binary struct definitions.""" + + def __init_subclass__(cls, **kwargs) -> None: + super().__init_subclass__(**kwargs) + if cls is BinaryStruct: + return + if not is_dataclass(cls): + dataclass(init=False)(cls) + fields = list(getattr(cls, '__annotations__', {}).items()) + cls.__binary_fields__ = fields # type: ignore[attr-defined] + + @classmethod + def _read(inner_cls, reader: BinaryReader): + obj = inner_cls.__new__(inner_cls) + for name, spec in inner_cls.__binary_fields__: + value = _parse_field(spec, reader, obj) + setattr(obj, name, value) + return obj + + cls._read = _read # type: ignore[attr-defined] + + @classmethod + def from_bytes(cls: type[T], data: bytes) -> T: + """Parse struct from bytes.""" + reader = BinaryReader(data) + return cls._read(reader) + + @classmethod + def _read(cls: type[T], reader: BinaryReader) -> T: + """Override in subclasses to implement parsing.""" + raise NotImplementedError + + +def _resolve_path(obj: Any, path: str) -> Any: + cur = obj + for part in path.split('.'): + cur = getattr(cur, part) + return cur + +def _unwrap_annotated(spec: Any) -> tuple[Any, ...]: + if get_origin(spec) is Annotated: + return get_args(spec)[1:] + return () + +def _field_type_from_spec(spec: Any) -> FieldType | None: + if isinstance(spec, FieldType): + return spec + for item in _unwrap_annotated(spec): + if isinstance(item, FieldType): + return item + return None + + +def _int_format(field_type: IntType) -> str: + if field_type.bits == 8: + return 'b' if field_type.signed else 'B' + endian = '>' if field_type.big_endian else '<' + if field_type.bits == 16: + code = 'h' if field_type.signed else 'H' + elif field_type.bits == 32: + code = 'i' if field_type.signed else 'I' + else: + raise ValueError(f"Unsupported integer size: {field_type.bits}") + return f"{endian}{code}" + +def _float_format(field_type: FloatType) -> str: + if field_type.bits == 32: + return ' Any: + field_type = _field_type_from_spec(spec) + if field_type is not None: + spec = field_type + if isinstance(spec, ConstType): + value = _parse_field(spec.base_type, reader, obj) + if value != spec.expected: + raise ValueError(f"Invalid constant: expected {spec.expected!r}, got {value!r}") + return value + if isinstance(spec, EnumType): + raw = _parse_field(spec.base_type, reader, obj) + try: + return spec.enum_cls(raw) + except ValueError: + return raw + if isinstance(spec, SwitchType): + key = _resolve_path(obj, spec.selector) + target = spec.cases.get(key, spec.default) + if target is None: + return None + return _parse_field(target, reader, obj) + if isinstance(spec, ArrayType): + count = _resolve_path(obj, spec.count_field) + return [_parse_field(spec.element_type, reader, obj) for _ in range(int(count))] + if isinstance(spec, SubstreamType): + length = _resolve_path(obj, spec.length_field) + data = reader.read_bytes(int(length)) + sub_reader = BinaryReader(data) + return _parse_field(spec.element_type, sub_reader, obj) + if isinstance(spec, IntType): + return reader._read_struct(_int_format(spec)) + if isinstance(spec, FloatType): + return reader._read_struct(_float_format(spec)) + if isinstance(spec, BitsType): + value = reader.read_bits_int_be(spec.bits) + return bool(value) if spec.bits == 1 else value + if isinstance(spec, BytesType): + return reader.read_bytes(spec.size) + if isinstance(spec, type) and issubclass(spec, BinaryStruct): + return spec._read(reader) + raise TypeError(f"Unsupported field spec: {spec!r}") diff --git a/system/ubloxd/generated/glonass.py b/system/ubloxd/generated/glonass.py deleted file mode 100644 index 40aa16bb6f..0000000000 --- a/system/ubloxd/generated/glonass.py +++ /dev/null @@ -1,247 +0,0 @@ -# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -import kaitaistruct -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO - - -if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): - raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) - -class Glonass(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.idle_chip = self._io.read_bits_int_be(1) != 0 - self.string_number = self._io.read_bits_int_be(4) - # workaround for kaitai bit alignment issue (see glonass_fix.patch for C++) - # self._io.align_to_byte() - _on = self.string_number - if _on == 4: - self.data = Glonass.String4(self._io, self, self._root) - elif _on == 1: - self.data = Glonass.String1(self._io, self, self._root) - elif _on == 3: - self.data = Glonass.String3(self._io, self, self._root) - elif _on == 5: - self.data = Glonass.String5(self._io, self, self._root) - elif _on == 2: - self.data = Glonass.String2(self._io, self, self._root) - else: - self.data = Glonass.StringNonImmediate(self._io, self, self._root) - self.hamming_code = self._io.read_bits_int_be(8) - self.pad_1 = self._io.read_bits_int_be(11) - self.superframe_number = self._io.read_bits_int_be(16) - self.pad_2 = self._io.read_bits_int_be(8) - self.frame_number = self._io.read_bits_int_be(8) - - class String4(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.tau_n_sign = self._io.read_bits_int_be(1) != 0 - self.tau_n_value = self._io.read_bits_int_be(21) - self.delta_tau_n_sign = self._io.read_bits_int_be(1) != 0 - self.delta_tau_n_value = self._io.read_bits_int_be(4) - self.e_n = self._io.read_bits_int_be(5) - self.not_used_1 = self._io.read_bits_int_be(14) - self.p4 = self._io.read_bits_int_be(1) != 0 - self.f_t = self._io.read_bits_int_be(4) - self.not_used_2 = self._io.read_bits_int_be(3) - self.n_t = self._io.read_bits_int_be(11) - self.n = self._io.read_bits_int_be(5) - self.m = self._io.read_bits_int_be(2) - - @property - def tau_n(self): - if hasattr(self, '_m_tau_n'): - return self._m_tau_n - - self._m_tau_n = ((self.tau_n_value * -1) if self.tau_n_sign else self.tau_n_value) - return getattr(self, '_m_tau_n', None) - - @property - def delta_tau_n(self): - if hasattr(self, '_m_delta_tau_n'): - return self._m_delta_tau_n - - self._m_delta_tau_n = ((self.delta_tau_n_value * -1) if self.delta_tau_n_sign else self.delta_tau_n_value) - return getattr(self, '_m_delta_tau_n', None) - - - class StringNonImmediate(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.data_1 = self._io.read_bits_int_be(64) - self.data_2 = self._io.read_bits_int_be(8) - - - class String5(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.n_a = self._io.read_bits_int_be(11) - self.tau_c = self._io.read_bits_int_be(32) - self.not_used = self._io.read_bits_int_be(1) != 0 - self.n_4 = self._io.read_bits_int_be(5) - self.tau_gps = self._io.read_bits_int_be(22) - self.l_n = self._io.read_bits_int_be(1) != 0 - - - class String1(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.not_used = self._io.read_bits_int_be(2) - self.p1 = self._io.read_bits_int_be(2) - self.t_k = self._io.read_bits_int_be(12) - self.x_vel_sign = self._io.read_bits_int_be(1) != 0 - self.x_vel_value = self._io.read_bits_int_be(23) - self.x_accel_sign = self._io.read_bits_int_be(1) != 0 - self.x_accel_value = self._io.read_bits_int_be(4) - self.x_sign = self._io.read_bits_int_be(1) != 0 - self.x_value = self._io.read_bits_int_be(26) - - @property - def x_vel(self): - if hasattr(self, '_m_x_vel'): - return self._m_x_vel - - self._m_x_vel = ((self.x_vel_value * -1) if self.x_vel_sign else self.x_vel_value) - return getattr(self, '_m_x_vel', None) - - @property - def x_accel(self): - if hasattr(self, '_m_x_accel'): - return self._m_x_accel - - self._m_x_accel = ((self.x_accel_value * -1) if self.x_accel_sign else self.x_accel_value) - return getattr(self, '_m_x_accel', None) - - @property - def x(self): - if hasattr(self, '_m_x'): - return self._m_x - - self._m_x = ((self.x_value * -1) if self.x_sign else self.x_value) - return getattr(self, '_m_x', None) - - - class String2(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.b_n = self._io.read_bits_int_be(3) - self.p2 = self._io.read_bits_int_be(1) != 0 - self.t_b = self._io.read_bits_int_be(7) - self.not_used = self._io.read_bits_int_be(5) - self.y_vel_sign = self._io.read_bits_int_be(1) != 0 - self.y_vel_value = self._io.read_bits_int_be(23) - self.y_accel_sign = self._io.read_bits_int_be(1) != 0 - self.y_accel_value = self._io.read_bits_int_be(4) - self.y_sign = self._io.read_bits_int_be(1) != 0 - self.y_value = self._io.read_bits_int_be(26) - - @property - def y_vel(self): - if hasattr(self, '_m_y_vel'): - return self._m_y_vel - - self._m_y_vel = ((self.y_vel_value * -1) if self.y_vel_sign else self.y_vel_value) - return getattr(self, '_m_y_vel', None) - - @property - def y_accel(self): - if hasattr(self, '_m_y_accel'): - return self._m_y_accel - - self._m_y_accel = ((self.y_accel_value * -1) if self.y_accel_sign else self.y_accel_value) - return getattr(self, '_m_y_accel', None) - - @property - def y(self): - if hasattr(self, '_m_y'): - return self._m_y - - self._m_y = ((self.y_value * -1) if self.y_sign else self.y_value) - return getattr(self, '_m_y', None) - - - class String3(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.p3 = self._io.read_bits_int_be(1) != 0 - self.gamma_n_sign = self._io.read_bits_int_be(1) != 0 - self.gamma_n_value = self._io.read_bits_int_be(10) - self.not_used = self._io.read_bits_int_be(1) != 0 - self.p = self._io.read_bits_int_be(2) - self.l_n = self._io.read_bits_int_be(1) != 0 - self.z_vel_sign = self._io.read_bits_int_be(1) != 0 - self.z_vel_value = self._io.read_bits_int_be(23) - self.z_accel_sign = self._io.read_bits_int_be(1) != 0 - self.z_accel_value = self._io.read_bits_int_be(4) - self.z_sign = self._io.read_bits_int_be(1) != 0 - self.z_value = self._io.read_bits_int_be(26) - - @property - def gamma_n(self): - if hasattr(self, '_m_gamma_n'): - return self._m_gamma_n - - self._m_gamma_n = ((self.gamma_n_value * -1) if self.gamma_n_sign else self.gamma_n_value) - return getattr(self, '_m_gamma_n', None) - - @property - def z_vel(self): - if hasattr(self, '_m_z_vel'): - return self._m_z_vel - - self._m_z_vel = ((self.z_vel_value * -1) if self.z_vel_sign else self.z_vel_value) - return getattr(self, '_m_z_vel', None) - - @property - def z_accel(self): - if hasattr(self, '_m_z_accel'): - return self._m_z_accel - - self._m_z_accel = ((self.z_accel_value * -1) if self.z_accel_sign else self.z_accel_value) - return getattr(self, '_m_z_accel', None) - - @property - def z(self): - if hasattr(self, '_m_z'): - return self._m_z - - self._m_z = ((self.z_value * -1) if self.z_sign else self.z_value) - return getattr(self, '_m_z', None) - - diff --git a/system/ubloxd/generated/gps.py b/system/ubloxd/generated/gps.py deleted file mode 100644 index a999016f3e..0000000000 --- a/system/ubloxd/generated/gps.py +++ /dev/null @@ -1,193 +0,0 @@ -# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -import kaitaistruct -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO - - -if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): - raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) - -class Gps(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.tlm = Gps.Tlm(self._io, self, self._root) - self.how = Gps.How(self._io, self, self._root) - _on = self.how.subframe_id - if _on == 1: - self.body = Gps.Subframe1(self._io, self, self._root) - elif _on == 2: - self.body = Gps.Subframe2(self._io, self, self._root) - elif _on == 3: - self.body = Gps.Subframe3(self._io, self, self._root) - elif _on == 4: - self.body = Gps.Subframe4(self._io, self, self._root) - - class Subframe1(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.week_no = self._io.read_bits_int_be(10) - self.code = self._io.read_bits_int_be(2) - self.sv_accuracy = self._io.read_bits_int_be(4) - self.sv_health = self._io.read_bits_int_be(6) - self.iodc_msb = self._io.read_bits_int_be(2) - self.l2_p_data_flag = self._io.read_bits_int_be(1) != 0 - self.reserved1 = self._io.read_bits_int_be(23) - self.reserved2 = self._io.read_bits_int_be(24) - self.reserved3 = self._io.read_bits_int_be(24) - self.reserved4 = self._io.read_bits_int_be(16) - self._io.align_to_byte() - self.t_gd = self._io.read_s1() - self.iodc_lsb = self._io.read_u1() - self.t_oc = self._io.read_u2be() - self.af_2 = self._io.read_s1() - self.af_1 = self._io.read_s2be() - self.af_0_sign = self._io.read_bits_int_be(1) != 0 - self.af_0_value = self._io.read_bits_int_be(21) - self.reserved5 = self._io.read_bits_int_be(2) - - @property - def af_0(self): - if hasattr(self, '_m_af_0'): - return self._m_af_0 - - self._m_af_0 = ((self.af_0_value - (1 << 21)) if self.af_0_sign else self.af_0_value) - return getattr(self, '_m_af_0', None) - - - class Subframe3(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.c_ic = self._io.read_s2be() - self.omega_0 = self._io.read_s4be() - self.c_is = self._io.read_s2be() - self.i_0 = self._io.read_s4be() - self.c_rc = self._io.read_s2be() - self.omega = self._io.read_s4be() - self.omega_dot_sign = self._io.read_bits_int_be(1) != 0 - self.omega_dot_value = self._io.read_bits_int_be(23) - self._io.align_to_byte() - self.iode = self._io.read_u1() - self.idot_sign = self._io.read_bits_int_be(1) != 0 - self.idot_value = self._io.read_bits_int_be(13) - self.reserved = self._io.read_bits_int_be(2) - - @property - def omega_dot(self): - if hasattr(self, '_m_omega_dot'): - return self._m_omega_dot - - self._m_omega_dot = ((self.omega_dot_value - (1 << 23)) if self.omega_dot_sign else self.omega_dot_value) - return getattr(self, '_m_omega_dot', None) - - @property - def idot(self): - if hasattr(self, '_m_idot'): - return self._m_idot - - self._m_idot = ((self.idot_value - (1 << 13)) if self.idot_sign else self.idot_value) - return getattr(self, '_m_idot', None) - - - class Subframe4(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.data_id = self._io.read_bits_int_be(2) - self.page_id = self._io.read_bits_int_be(6) - self._io.align_to_byte() - _on = self.page_id - if _on == 56: - self.body = Gps.Subframe4.IonosphereData(self._io, self, self._root) - - class IonosphereData(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.a0 = self._io.read_s1() - self.a1 = self._io.read_s1() - self.a2 = self._io.read_s1() - self.a3 = self._io.read_s1() - self.b0 = self._io.read_s1() - self.b1 = self._io.read_s1() - self.b2 = self._io.read_s1() - self.b3 = self._io.read_s1() - - - - class How(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.tow_count = self._io.read_bits_int_be(17) - self.alert = self._io.read_bits_int_be(1) != 0 - self.anti_spoof = self._io.read_bits_int_be(1) != 0 - self.subframe_id = self._io.read_bits_int_be(3) - self.reserved = self._io.read_bits_int_be(2) - - - class Tlm(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.preamble = self._io.read_bytes(1) - if not self.preamble == b"\x8B": - raise kaitaistruct.ValidationNotEqualError(b"\x8B", self.preamble, self._io, u"/types/tlm/seq/0") - self.tlm = self._io.read_bits_int_be(14) - self.integrity_status = self._io.read_bits_int_be(1) != 0 - self.reserved = self._io.read_bits_int_be(1) != 0 - - - class Subframe2(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.iode = self._io.read_u1() - self.c_rs = self._io.read_s2be() - self.delta_n = self._io.read_s2be() - self.m_0 = self._io.read_s4be() - self.c_uc = self._io.read_s2be() - self.e = self._io.read_s4be() - self.c_us = self._io.read_s2be() - self.sqrt_a = self._io.read_u4be() - self.t_oe = self._io.read_u2be() - self.fit_interval_flag = self._io.read_bits_int_be(1) != 0 - self.aoda = self._io.read_bits_int_be(5) - self.reserved = self._io.read_bits_int_be(2) - - - diff --git a/system/ubloxd/generated/ubx.py b/system/ubloxd/generated/ubx.py deleted file mode 100644 index 9946584388..0000000000 --- a/system/ubloxd/generated/ubx.py +++ /dev/null @@ -1,273 +0,0 @@ -# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -import kaitaistruct -from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -from enum import Enum - - -if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): - raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) - -class Ubx(KaitaiStruct): - - class GnssType(Enum): - gps = 0 - sbas = 1 - galileo = 2 - beidou = 3 - imes = 4 - qzss = 5 - glonass = 6 - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.magic = self._io.read_bytes(2) - if not self.magic == b"\xB5\x62": - raise kaitaistruct.ValidationNotEqualError(b"\xB5\x62", self.magic, self._io, u"/seq/0") - self.msg_type = self._io.read_u2be() - self.length = self._io.read_u2le() - _on = self.msg_type - if _on == 2569: - self.body = Ubx.MonHw(self._io, self, self._root) - elif _on == 533: - self.body = Ubx.RxmRawx(self._io, self, self._root) - elif _on == 531: - self.body = Ubx.RxmSfrbx(self._io, self, self._root) - elif _on == 309: - self.body = Ubx.NavSat(self._io, self, self._root) - elif _on == 2571: - self.body = Ubx.MonHw2(self._io, self, self._root) - elif _on == 263: - self.body = Ubx.NavPvt(self._io, self, self._root) - - class RxmRawx(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.rcv_tow = self._io.read_f8le() - self.week = self._io.read_u2le() - self.leap_s = self._io.read_s1() - self.num_meas = self._io.read_u1() - self.rec_stat = self._io.read_u1() - self.reserved1 = self._io.read_bytes(3) - self._raw_meas = [] - self.meas = [] - for i in range(self.num_meas): - self._raw_meas.append(self._io.read_bytes(32)) - _io__raw_meas = KaitaiStream(BytesIO(self._raw_meas[i])) - self.meas.append(Ubx.RxmRawx.Measurement(_io__raw_meas, self, self._root)) - - - class Measurement(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.pr_mes = self._io.read_f8le() - self.cp_mes = self._io.read_f8le() - self.do_mes = self._io.read_f4le() - self.gnss_id = KaitaiStream.resolve_enum(Ubx.GnssType, self._io.read_u1()) - self.sv_id = self._io.read_u1() - self.reserved2 = self._io.read_bytes(1) - self.freq_id = self._io.read_u1() - self.lock_time = self._io.read_u2le() - self.cno = self._io.read_u1() - self.pr_stdev = self._io.read_u1() - self.cp_stdev = self._io.read_u1() - self.do_stdev = self._io.read_u1() - self.trk_stat = self._io.read_u1() - self.reserved3 = self._io.read_bytes(1) - - - - class RxmSfrbx(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.gnss_id = KaitaiStream.resolve_enum(Ubx.GnssType, self._io.read_u1()) - self.sv_id = self._io.read_u1() - self.reserved1 = self._io.read_bytes(1) - self.freq_id = self._io.read_u1() - self.num_words = self._io.read_u1() - self.reserved2 = self._io.read_bytes(1) - self.version = self._io.read_u1() - self.reserved3 = self._io.read_bytes(1) - self.body = [] - for i in range(self.num_words): - self.body.append(self._io.read_u4le()) - - - - class NavSat(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.itow = self._io.read_u4le() - self.version = self._io.read_u1() - self.num_svs = self._io.read_u1() - self.reserved = self._io.read_bytes(2) - self._raw_svs = [] - self.svs = [] - for i in range(self.num_svs): - self._raw_svs.append(self._io.read_bytes(12)) - _io__raw_svs = KaitaiStream(BytesIO(self._raw_svs[i])) - self.svs.append(Ubx.NavSat.Nav(_io__raw_svs, self, self._root)) - - - class Nav(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.gnss_id = KaitaiStream.resolve_enum(Ubx.GnssType, self._io.read_u1()) - self.sv_id = self._io.read_u1() - self.cno = self._io.read_u1() - self.elev = self._io.read_s1() - self.azim = self._io.read_s2le() - self.pr_res = self._io.read_s2le() - self.flags = self._io.read_u4le() - - - - class NavPvt(KaitaiStruct): - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.i_tow = self._io.read_u4le() - self.year = self._io.read_u2le() - self.month = self._io.read_u1() - self.day = self._io.read_u1() - self.hour = self._io.read_u1() - self.min = self._io.read_u1() - self.sec = self._io.read_u1() - self.valid = self._io.read_u1() - self.t_acc = self._io.read_u4le() - self.nano = self._io.read_s4le() - self.fix_type = self._io.read_u1() - self.flags = self._io.read_u1() - self.flags2 = self._io.read_u1() - self.num_sv = self._io.read_u1() - self.lon = self._io.read_s4le() - self.lat = self._io.read_s4le() - self.height = self._io.read_s4le() - self.h_msl = self._io.read_s4le() - self.h_acc = self._io.read_u4le() - self.v_acc = self._io.read_u4le() - self.vel_n = self._io.read_s4le() - self.vel_e = self._io.read_s4le() - self.vel_d = self._io.read_s4le() - self.g_speed = self._io.read_s4le() - self.head_mot = self._io.read_s4le() - self.s_acc = self._io.read_s4le() - self.head_acc = self._io.read_u4le() - self.p_dop = self._io.read_u2le() - self.flags3 = self._io.read_u1() - self.reserved1 = self._io.read_bytes(5) - self.head_veh = self._io.read_s4le() - self.mag_dec = self._io.read_s2le() - self.mag_acc = self._io.read_u2le() - - - class MonHw2(KaitaiStruct): - - class ConfigSource(Enum): - flash = 102 - otp = 111 - config_pins = 112 - rom = 113 - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.ofs_i = self._io.read_s1() - self.mag_i = self._io.read_u1() - self.ofs_q = self._io.read_s1() - self.mag_q = self._io.read_u1() - self.cfg_source = KaitaiStream.resolve_enum(Ubx.MonHw2.ConfigSource, self._io.read_u1()) - self.reserved1 = self._io.read_bytes(3) - self.low_lev_cfg = self._io.read_u4le() - self.reserved2 = self._io.read_bytes(8) - self.post_status = self._io.read_u4le() - self.reserved3 = self._io.read_bytes(4) - - - class MonHw(KaitaiStruct): - - class AntennaStatus(Enum): - init = 0 - dontknow = 1 - ok = 2 - short = 3 - open = 4 - - class AntennaPower(Enum): - false = 0 - true = 1 - dontknow = 2 - def __init__(self, _io, _parent=None, _root=None): - self._io = _io - self._parent = _parent - self._root = _root if _root else self - self._read() - - def _read(self): - self.pin_sel = self._io.read_u4le() - self.pin_bank = self._io.read_u4le() - self.pin_dir = self._io.read_u4le() - self.pin_val = self._io.read_u4le() - self.noise_per_ms = self._io.read_u2le() - self.agc_cnt = self._io.read_u2le() - self.a_status = KaitaiStream.resolve_enum(Ubx.MonHw.AntennaStatus, self._io.read_u1()) - self.a_power = KaitaiStream.resolve_enum(Ubx.MonHw.AntennaPower, self._io.read_u1()) - self.flags = self._io.read_u1() - self.reserved1 = self._io.read_bytes(1) - self.used_mask = self._io.read_u4le() - self.vp = self._io.read_bytes(17) - self.jam_ind = self._io.read_u1() - self.reserved2 = self._io.read_bytes(2) - self.pin_irq = self._io.read_u4le() - self.pull_h = self._io.read_u4le() - self.pull_l = self._io.read_u4le() - - - @property - def checksum(self): - if hasattr(self, '_m_checksum'): - return self._m_checksum - - _pos = self._io.pos() - self._io.seek((self.length + 6)) - self._m_checksum = self._io.read_u2le() - self._io.seek(_pos) - return getattr(self, '_m_checksum', None) - - diff --git a/system/ubloxd/glonass.ksy b/system/ubloxd/glonass.ksy deleted file mode 100644 index be99f6e497..0000000000 --- a/system/ubloxd/glonass.ksy +++ /dev/null @@ -1,176 +0,0 @@ -# http://gauss.gge.unb.ca/GLONASS.ICD.pdf -# some variables are misprinted but good in the old doc -# https://www.unavco.org/help/glossary/docs/ICD_GLONASS_4.0_(1998)_en.pdf -meta: - id: glonass - endian: be - bit-endian: be -seq: - - id: idle_chip - type: b1 - - id: string_number - type: b4 - - id: data - type: - switch-on: string_number - cases: - 1: string_1 - 2: string_2 - 3: string_3 - 4: string_4 - 5: string_5 - _: string_non_immediate - - id: hamming_code - type: b8 - - id: pad_1 - type: b11 - - id: superframe_number - type: b16 - - id: pad_2 - type: b8 - - id: frame_number - type: b8 - -types: - string_1: - seq: - - id: not_used - type: b2 - - id: p1 - type: b2 - - id: t_k - type: b12 - - id: x_vel_sign - type: b1 - - id: x_vel_value - type: b23 - - id: x_accel_sign - type: b1 - - id: x_accel_value - type: b4 - - id: x_sign - type: b1 - - id: x_value - type: b26 - instances: - x_vel: - value: 'x_vel_sign ? (x_vel_value * (-1)) : x_vel_value' - x_accel: - value: 'x_accel_sign ? (x_accel_value * (-1)) : x_accel_value' - x: - value: 'x_sign ? (x_value * (-1)) : x_value' - string_2: - seq: - - id: b_n - type: b3 - - id: p2 - type: b1 - - id: t_b - type: b7 - - id: not_used - type: b5 - - id: y_vel_sign - type: b1 - - id: y_vel_value - type: b23 - - id: y_accel_sign - type: b1 - - id: y_accel_value - type: b4 - - id: y_sign - type: b1 - - id: y_value - type: b26 - instances: - y_vel: - value: 'y_vel_sign ? (y_vel_value * (-1)) : y_vel_value' - y_accel: - value: 'y_accel_sign ? (y_accel_value * (-1)) : y_accel_value' - y: - value: 'y_sign ? (y_value * (-1)) : y_value' - string_3: - seq: - - id: p3 - type: b1 - - id: gamma_n_sign - type: b1 - - id: gamma_n_value - type: b10 - - id: not_used - type: b1 - - id: p - type: b2 - - id: l_n - type: b1 - - id: z_vel_sign - type: b1 - - id: z_vel_value - type: b23 - - id: z_accel_sign - type: b1 - - id: z_accel_value - type: b4 - - id: z_sign - type: b1 - - id: z_value - type: b26 - instances: - gamma_n: - value: 'gamma_n_sign ? (gamma_n_value * (-1)) : gamma_n_value' - z_vel: - value: 'z_vel_sign ? (z_vel_value * (-1)) : z_vel_value' - z_accel: - value: 'z_accel_sign ? (z_accel_value * (-1)) : z_accel_value' - z: - value: 'z_sign ? (z_value * (-1)) : z_value' - string_4: - seq: - - id: tau_n_sign - type: b1 - - id: tau_n_value - type: b21 - - id: delta_tau_n_sign - type: b1 - - id: delta_tau_n_value - type: b4 - - id: e_n - type: b5 - - id: not_used_1 - type: b14 - - id: p4 - type: b1 - - id: f_t - type: b4 - - id: not_used_2 - type: b3 - - id: n_t - type: b11 - - id: n - type: b5 - - id: m - type: b2 - instances: - tau_n: - value: 'tau_n_sign ? (tau_n_value * (-1)) : tau_n_value' - delta_tau_n: - value: 'delta_tau_n_sign ? (delta_tau_n_value * (-1)) : delta_tau_n_value' - string_5: - seq: - - id: n_a - type: b11 - - id: tau_c - type: b32 - - id: not_used - type: b1 - - id: n_4 - type: b5 - - id: tau_gps - type: b22 - - id: l_n - type: b1 - string_non_immediate: - seq: - - id: data_1 - type: b64 - - id: data_2 - type: b8 diff --git a/system/ubloxd/glonass.py b/system/ubloxd/glonass.py new file mode 100644 index 0000000000..144ccdde6e --- /dev/null +++ b/system/ubloxd/glonass.py @@ -0,0 +1,156 @@ +""" +Parses GLONASS navigation strings per GLONASS ICD specification. +http://gauss.gge.unb.ca/GLONASS.ICD.pdf +https://www.unavco.org/help/glossary/docs/ICD_GLONASS_4.0_(1998)_en.pdf +""" + +from typing import Annotated + +from openpilot.system.ubloxd import binary_struct as bs + + +class Glonass(bs.BinaryStruct): + class String1(bs.BinaryStruct): + not_used: Annotated[int, bs.bits(2)] + p1: Annotated[int, bs.bits(2)] + t_k: Annotated[int, bs.bits(12)] + x_vel_sign: Annotated[bool, bs.bits(1)] + x_vel_value: Annotated[int, bs.bits(23)] + x_accel_sign: Annotated[bool, bs.bits(1)] + x_accel_value: Annotated[int, bs.bits(4)] + x_sign: Annotated[bool, bs.bits(1)] + x_value: Annotated[int, bs.bits(26)] + + @property + def x_vel(self) -> int: + """Computed x_vel from sign-magnitude representation.""" + return (self.x_vel_value * -1) if self.x_vel_sign else self.x_vel_value + + @property + def x_accel(self) -> int: + """Computed x_accel from sign-magnitude representation.""" + return (self.x_accel_value * -1) if self.x_accel_sign else self.x_accel_value + + @property + def x(self) -> int: + """Computed x from sign-magnitude representation.""" + return (self.x_value * -1) if self.x_sign else self.x_value + + class String2(bs.BinaryStruct): + b_n: Annotated[int, bs.bits(3)] + p2: Annotated[bool, bs.bits(1)] + t_b: Annotated[int, bs.bits(7)] + not_used: Annotated[int, bs.bits(5)] + y_vel_sign: Annotated[bool, bs.bits(1)] + y_vel_value: Annotated[int, bs.bits(23)] + y_accel_sign: Annotated[bool, bs.bits(1)] + y_accel_value: Annotated[int, bs.bits(4)] + y_sign: Annotated[bool, bs.bits(1)] + y_value: Annotated[int, bs.bits(26)] + + @property + def y_vel(self) -> int: + """Computed y_vel from sign-magnitude representation.""" + return (self.y_vel_value * -1) if self.y_vel_sign else self.y_vel_value + + @property + def y_accel(self) -> int: + """Computed y_accel from sign-magnitude representation.""" + return (self.y_accel_value * -1) if self.y_accel_sign else self.y_accel_value + + @property + def y(self) -> int: + """Computed y from sign-magnitude representation.""" + return (self.y_value * -1) if self.y_sign else self.y_value + + class String3(bs.BinaryStruct): + p3: Annotated[bool, bs.bits(1)] + gamma_n_sign: Annotated[bool, bs.bits(1)] + gamma_n_value: Annotated[int, bs.bits(10)] + not_used: Annotated[bool, bs.bits(1)] + p: Annotated[int, bs.bits(2)] + l_n: Annotated[bool, bs.bits(1)] + z_vel_sign: Annotated[bool, bs.bits(1)] + z_vel_value: Annotated[int, bs.bits(23)] + z_accel_sign: Annotated[bool, bs.bits(1)] + z_accel_value: Annotated[int, bs.bits(4)] + z_sign: Annotated[bool, bs.bits(1)] + z_value: Annotated[int, bs.bits(26)] + + @property + def gamma_n(self) -> int: + """Computed gamma_n from sign-magnitude representation.""" + return (self.gamma_n_value * -1) if self.gamma_n_sign else self.gamma_n_value + + @property + def z_vel(self) -> int: + """Computed z_vel from sign-magnitude representation.""" + return (self.z_vel_value * -1) if self.z_vel_sign else self.z_vel_value + + @property + def z_accel(self) -> int: + """Computed z_accel from sign-magnitude representation.""" + return (self.z_accel_value * -1) if self.z_accel_sign else self.z_accel_value + + @property + def z(self) -> int: + """Computed z from sign-magnitude representation.""" + return (self.z_value * -1) if self.z_sign else self.z_value + + class String4(bs.BinaryStruct): + tau_n_sign: Annotated[bool, bs.bits(1)] + tau_n_value: Annotated[int, bs.bits(21)] + delta_tau_n_sign: Annotated[bool, bs.bits(1)] + delta_tau_n_value: Annotated[int, bs.bits(4)] + e_n: Annotated[int, bs.bits(5)] + not_used_1: Annotated[int, bs.bits(14)] + p4: Annotated[bool, bs.bits(1)] + f_t: Annotated[int, bs.bits(4)] + not_used_2: Annotated[int, bs.bits(3)] + n_t: Annotated[int, bs.bits(11)] + n: Annotated[int, bs.bits(5)] + m: Annotated[int, bs.bits(2)] + + @property + def tau_n(self) -> int: + """Computed tau_n from sign-magnitude representation.""" + return (self.tau_n_value * -1) if self.tau_n_sign else self.tau_n_value + + @property + def delta_tau_n(self) -> int: + """Computed delta_tau_n from sign-magnitude representation.""" + return (self.delta_tau_n_value * -1) if self.delta_tau_n_sign else self.delta_tau_n_value + + class String5(bs.BinaryStruct): + n_a: Annotated[int, bs.bits(11)] + tau_c: Annotated[int, bs.bits(32)] + not_used: Annotated[bool, bs.bits(1)] + n_4: Annotated[int, bs.bits(5)] + tau_gps: Annotated[int, bs.bits(22)] + l_n: Annotated[bool, bs.bits(1)] + + class StringNonImmediate(bs.BinaryStruct): + data_1: Annotated[int, bs.bits(64)] + data_2: Annotated[int, bs.bits(8)] + + idle_chip: Annotated[bool, bs.bits(1)] + string_number: Annotated[int, bs.bits(4)] + data: Annotated[ + object, + bs.switch( + 'string_number', + { + 1: String1, + 2: String2, + 3: String3, + 4: String4, + 5: String5, + }, + default=StringNonImmediate, + ), + ] + hamming_code: Annotated[int, bs.bits(8)] + pad_1: Annotated[int, bs.bits(11)] + superframe_number: Annotated[int, bs.bits(16)] + pad_2: Annotated[int, bs.bits(8)] + frame_number: Annotated[int, bs.bits(8)] diff --git a/system/ubloxd/gps.ksy b/system/ubloxd/gps.ksy deleted file mode 100644 index 893ad1b25b..0000000000 --- a/system/ubloxd/gps.ksy +++ /dev/null @@ -1,189 +0,0 @@ -# https://www.gps.gov/technical/icwg/IS-GPS-200E.pdf -meta: - id: gps - endian: be - bit-endian: be -seq: - - id: tlm - type: tlm - - id: how - type: how - - id: body - type: - switch-on: how.subframe_id - cases: - 1: subframe_1 - 2: subframe_2 - 3: subframe_3 - 4: subframe_4 -types: - tlm: - seq: - - id: preamble - contents: [0x8b] - - id: tlm - type: b14 - - id: integrity_status - type: b1 - - id: reserved - type: b1 - how: - seq: - - id: tow_count - type: b17 - - id: alert - type: b1 - - id: anti_spoof - type: b1 - - id: subframe_id - type: b3 - - id: reserved - type: b2 - subframe_1: - seq: - # Word 3 - - id: week_no - type: b10 - - id: code - type: b2 - - id: sv_accuracy - type: b4 - - id: sv_health - type: b6 - - id: iodc_msb - type: b2 - # Word 4 - - id: l2_p_data_flag - type: b1 - - id: reserved1 - type: b23 - # Word 5 - - id: reserved2 - type: b24 - # Word 6 - - id: reserved3 - type: b24 - # Word 7 - - id: reserved4 - type: b16 - - id: t_gd - type: s1 - # Word 8 - - id: iodc_lsb - type: u1 - - id: t_oc - type: u2 - # Word 9 - - id: af_2 - type: s1 - - id: af_1 - type: s2 - # Word 10 - - id: af_0_sign - type: b1 - - id: af_0_value - type: b21 - - id: reserved5 - type: b2 - instances: - af_0: - value: 'af_0_sign ? (af_0_value - (1 << 21)) : af_0_value' - subframe_2: - seq: - # Word 3 - - id: iode - type: u1 - - id: c_rs - type: s2 - # Word 4 & 5 - - id: delta_n - type: s2 - - id: m_0 - type: s4 - # Word 6 & 7 - - id: c_uc - type: s2 - - id: e - type: s4 - # Word 8 & 9 - - id: c_us - type: s2 - - id: sqrt_a - type: u4 - # Word 10 - - id: t_oe - type: u2 - - id: fit_interval_flag - type: b1 - - id: aoda - type: b5 - - id: reserved - type: b2 - subframe_3: - seq: - # Word 3 & 4 - - id: c_ic - type: s2 - - id: omega_0 - type: s4 - # Word 5 & 6 - - id: c_is - type: s2 - - id: i_0 - type: s4 - # Word 7 & 8 - - id: c_rc - type: s2 - - id: omega - type: s4 - # Word 9 - - id: omega_dot_sign - type: b1 - - id: omega_dot_value - type: b23 - # Word 10 - - id: iode - type: u1 - - id: idot_sign - type: b1 - - id: idot_value - type: b13 - - id: reserved - type: b2 - instances: - omega_dot: - value: 'omega_dot_sign ? (omega_dot_value - (1 << 23)) : omega_dot_value' - idot: - value: 'idot_sign ? (idot_value - (1 << 13)) : idot_value' - subframe_4: - seq: - # Word 3 - - id: data_id - type: b2 - - id: page_id - type: b6 - - id: body - type: - switch-on: page_id - cases: - 56: ionosphere_data - types: - ionosphere_data: - seq: - - id: a0 - type: s1 - - id: a1 - type: s1 - - id: a2 - type: s1 - - id: a3 - type: s1 - - id: b0 - type: s1 - - id: b1 - type: s1 - - id: b2 - type: s1 - - id: b3 - type: s1 - diff --git a/system/ubloxd/gps.py b/system/ubloxd/gps.py new file mode 100644 index 0000000000..1c0833bd92 --- /dev/null +++ b/system/ubloxd/gps.py @@ -0,0 +1,116 @@ +""" +Parses GPS navigation subframes per IS-GPS-200E specification. +https://www.gps.gov/technical/icwg/IS-GPS-200E.pdf +""" + +from typing import Annotated + +from openpilot.system.ubloxd import binary_struct as bs + + +class Gps(bs.BinaryStruct): + class Tlm(bs.BinaryStruct): + preamble: Annotated[bytes, bs.const(bs.bytes_field(1), b"\x8b")] + tlm: Annotated[int, bs.bits(14)] + integrity_status: Annotated[bool, bs.bits(1)] + reserved: Annotated[bool, bs.bits(1)] + + class How(bs.BinaryStruct): + tow_count: Annotated[int, bs.bits(17)] + alert: Annotated[bool, bs.bits(1)] + anti_spoof: Annotated[bool, bs.bits(1)] + subframe_id: Annotated[int, bs.bits(3)] + reserved: Annotated[int, bs.bits(2)] + + class Subframe1(bs.BinaryStruct): + week_no: Annotated[int, bs.bits(10)] + code: Annotated[int, bs.bits(2)] + sv_accuracy: Annotated[int, bs.bits(4)] + sv_health: Annotated[int, bs.bits(6)] + iodc_msb: Annotated[int, bs.bits(2)] + l2_p_data_flag: Annotated[bool, bs.bits(1)] + reserved1: Annotated[int, bs.bits(23)] + reserved2: Annotated[int, bs.bits(24)] + reserved3: Annotated[int, bs.bits(24)] + reserved4: Annotated[int, bs.bits(16)] + t_gd: Annotated[int, bs.s8] + iodc_lsb: Annotated[int, bs.u8] + t_oc: Annotated[int, bs.u16be] + af_2: Annotated[int, bs.s8] + af_1: Annotated[int, bs.s16be] + af_0_sign: Annotated[bool, bs.bits(1)] + af_0_value: Annotated[int, bs.bits(21)] + reserved5: Annotated[int, bs.bits(2)] + + @property + def af_0(self) -> int: + """Computed af_0 from sign-magnitude representation.""" + return (self.af_0_value - (1 << 21)) if self.af_0_sign else self.af_0_value + + class Subframe2(bs.BinaryStruct): + iode: Annotated[int, bs.u8] + c_rs: Annotated[int, bs.s16be] + delta_n: Annotated[int, bs.s16be] + m_0: Annotated[int, bs.s32be] + c_uc: Annotated[int, bs.s16be] + e: Annotated[int, bs.s32be] + c_us: Annotated[int, bs.s16be] + sqrt_a: Annotated[int, bs.u32be] + t_oe: Annotated[int, bs.u16be] + fit_interval_flag: Annotated[bool, bs.bits(1)] + aoda: Annotated[int, bs.bits(5)] + reserved: Annotated[int, bs.bits(2)] + + class Subframe3(bs.BinaryStruct): + c_ic: Annotated[int, bs.s16be] + omega_0: Annotated[int, bs.s32be] + c_is: Annotated[int, bs.s16be] + i_0: Annotated[int, bs.s32be] + c_rc: Annotated[int, bs.s16be] + omega: Annotated[int, bs.s32be] + omega_dot_sign: Annotated[bool, bs.bits(1)] + omega_dot_value: Annotated[int, bs.bits(23)] + iode: Annotated[int, bs.u8] + idot_sign: Annotated[bool, bs.bits(1)] + idot_value: Annotated[int, bs.bits(13)] + reserved: Annotated[int, bs.bits(2)] + + @property + def omega_dot(self) -> int: + """Computed omega_dot from sign-magnitude representation.""" + return (self.omega_dot_value - (1 << 23)) if self.omega_dot_sign else self.omega_dot_value + + @property + def idot(self) -> int: + """Computed idot from sign-magnitude representation.""" + return (self.idot_value - (1 << 13)) if self.idot_sign else self.idot_value + + class Subframe4(bs.BinaryStruct): + class IonosphereData(bs.BinaryStruct): + a0: Annotated[int, bs.s8] + a1: Annotated[int, bs.s8] + a2: Annotated[int, bs.s8] + a3: Annotated[int, bs.s8] + b0: Annotated[int, bs.s8] + b1: Annotated[int, bs.s8] + b2: Annotated[int, bs.s8] + b3: Annotated[int, bs.s8] + + data_id: Annotated[int, bs.bits(2)] + page_id: Annotated[int, bs.bits(6)] + body: Annotated[object, bs.switch('page_id', {56: IonosphereData})] + + tlm: Tlm + how: How + body: Annotated[ + object, + bs.switch( + 'how.subframe_id', + { + 1: Subframe1, + 2: Subframe2, + 3: Subframe3, + 4: Subframe4, + }, + ), + ] diff --git a/system/ubloxd/ubloxd.py b/system/ubloxd/ubloxd.py index 6882ad0955..e55cadcf78 100755 --- a/system/ubloxd/ubloxd.py +++ b/system/ubloxd/ubloxd.py @@ -8,9 +8,9 @@ from dataclasses import dataclass from cereal import log from cereal import messaging -from openpilot.system.ubloxd.generated.ubx import Ubx -from openpilot.system.ubloxd.generated.gps import Gps -from openpilot.system.ubloxd.generated.glonass import Glonass +from openpilot.system.ubloxd.ubx import Ubx +from openpilot.system.ubloxd.gps import Gps +from openpilot.system.ubloxd.glonass import Glonass SECS_IN_MIN = 60 @@ -52,7 +52,7 @@ class UbxFramer: # find preamble if len(self.buf) < 2: break - start = self.buf.find(b"\xB5\x62") + start = self.buf.find(b"\xb5\x62") if start < 0: # no preamble in buffer self.buf.clear() @@ -98,9 +98,22 @@ class UbloxMsgParser: # user range accuracy in meters glonass_URA_lookup: dict[int, float] = { - 0: 1, 1: 2, 2: 2.5, 3: 4, 4: 5, 5: 7, - 6: 10, 7: 12, 8: 14, 9: 16, 10: 32, - 11: 64, 12: 128, 13: 256, 14: 512, 15: 1024, + 0: 1, + 1: 2, + 2: 2.5, + 3: 4, + 4: 5, + 5: 7, + 6: 10, + 7: 12, + 8: 14, + 9: 16, + 10: 32, + 11: 64, + 12: 128, + 13: 256, + 14: 512, + 15: 1024, } def __init__(self) -> None: @@ -121,7 +134,7 @@ class UbloxMsgParser: body = Ubx.NavPvt.from_bytes(payload) return self._gen_nav_pvt(body) if msg_type == 0x0213: - # Manually parse RXM-SFRBX to avoid Kaitai EOF on some frames + # Manually parse RXM-SFRBX to avoid EOF on some frames if len(payload) < 8: return None gnss_id = payload[0] @@ -134,7 +147,7 @@ class UbloxMsgParser: words: list[int] = [] off = 8 for _ in range(num_words): - words.append(int.from_bytes(payload[off:off+4], 'little')) + words.append(int.from_bytes(payload[off : off + 4], 'little')) off += 4 class _SfrbxView: @@ -143,6 +156,7 @@ class UbloxMsgParser: self.sv_id = sid self.freq_id = fid self.body = body + view = _SfrbxView(gnss_id, sv_id, freq_id, words) return self._gen_rxm_sfrbx(view) if msg_type == 0x0215: @@ -515,5 +529,6 @@ def main(): service, dat = res pm.send(service, dat) + if __name__ == '__main__': main() diff --git a/system/ubloxd/ubx.ksy b/system/ubloxd/ubx.ksy deleted file mode 100644 index 02c757fe71..0000000000 --- a/system/ubloxd/ubx.ksy +++ /dev/null @@ -1,293 +0,0 @@ -meta: - id: ubx - endian: le -seq: - - id: magic - contents: [0xb5, 0x62] - - id: msg_type - type: u2be - - id: length - type: u2 - - id: body - type: - switch-on: msg_type - cases: - 0x0107: nav_pvt - 0x0213: rxm_sfrbx - 0x0215: rxm_rawx - 0x0a09: mon_hw - 0x0a0b: mon_hw2 - 0x0135: nav_sat -instances: - checksum: - pos: length + 6 - type: u2 - -types: - mon_hw: - seq: - - id: pin_sel - type: u4 - - id: pin_bank - type: u4 - - id: pin_dir - type: u4 - - id: pin_val - type: u4 - - id: noise_per_ms - type: u2 - - id: agc_cnt - type: u2 - - id: a_status - type: u1 - enum: antenna_status - - id: a_power - type: u1 - enum: antenna_power - - id: flags - type: u1 - - id: reserved1 - size: 1 - - id: used_mask - type: u4 - - id: vp - size: 17 - - id: jam_ind - type: u1 - - id: reserved2 - size: 2 - - id: pin_irq - type: u4 - - id: pull_h - type: u4 - - id: pull_l - type: u4 - enums: - antenna_status: - 0: init - 1: dontknow - 2: ok - 3: short - 4: open - antenna_power: - 0: off - 1: on - 2: dontknow - - mon_hw2: - seq: - - id: ofs_i - type: s1 - - id: mag_i - type: u1 - - id: ofs_q - type: s1 - - id: mag_q - type: u1 - - id: cfg_source - type: u1 - enum: config_source - - id: reserved1 - size: 3 - - id: low_lev_cfg - type: u4 - - id: reserved2 - size: 8 - - id: post_status - type: u4 - - id: reserved3 - size: 4 - - enums: - config_source: - 113: rom - 111: otp - 112: config_pins - 102: flash - - rxm_sfrbx: - seq: - - id: gnss_id - type: u1 - enum: gnss_type - - id: sv_id - type: u1 - - id: reserved1 - size: 1 - - id: freq_id - type: u1 - - id: num_words - type: u1 - - id: reserved2 - size: 1 - - id: version - type: u1 - - id: reserved3 - size: 1 - - id: body - type: u4 - repeat: expr - repeat-expr: num_words - - rxm_rawx: - seq: - - id: rcv_tow - type: f8 - - id: week - type: u2 - - id: leap_s - type: s1 - - id: num_meas - type: u1 - - id: rec_stat - type: u1 - - id: reserved1 - size: 3 - - id: meas - type: measurement - size: 32 - repeat: expr - repeat-expr: num_meas - types: - measurement: - seq: - - id: pr_mes - type: f8 - - id: cp_mes - type: f8 - - id: do_mes - type: f4 - - id: gnss_id - type: u1 - enum: gnss_type - - id: sv_id - type: u1 - - id: reserved2 - size: 1 - - id: freq_id - type: u1 - - id: lock_time - type: u2 - - id: cno - type: u1 - - id: pr_stdev - type: u1 - - id: cp_stdev - type: u1 - - id: do_stdev - type: u1 - - id: trk_stat - type: u1 - - id: reserved3 - size: 1 - nav_sat: - seq: - - id: itow - type: u4 - - id: version - type: u1 - - id: num_svs - type: u1 - - id: reserved - size: 2 - - id: svs - type: nav - size: 12 - repeat: expr - repeat-expr: num_svs - types: - nav: - seq: - - id: gnss_id - type: u1 - enum: gnss_type - - id: sv_id - type: u1 - - id: cno - type: u1 - - id: elev - type: s1 - - id: azim - type: s2 - - id: pr_res - type: s2 - - id: flags - type: u4 - - nav_pvt: - seq: - - id: i_tow - type: u4 - - id: year - type: u2 - - id: month - type: u1 - - id: day - type: u1 - - id: hour - type: u1 - - id: min - type: u1 - - id: sec - type: u1 - - id: valid - type: u1 - - id: t_acc - type: u4 - - id: nano - type: s4 - - id: fix_type - type: u1 - - id: flags - type: u1 - - id: flags2 - type: u1 - - id: num_sv - type: u1 - - id: lon - type: s4 - - id: lat - type: s4 - - id: height - type: s4 - - id: h_msl - type: s4 - - id: h_acc - type: u4 - - id: v_acc - type: u4 - - id: vel_n - type: s4 - - id: vel_e - type: s4 - - id: vel_d - type: s4 - - id: g_speed - type: s4 - - id: head_mot - type: s4 - - id: s_acc - type: s4 - - id: head_acc - type: u4 - - id: p_dop - type: u2 - - id: flags3 - type: u1 - - id: reserved1 - size: 5 - - id: head_veh - type: s4 - - id: mag_dec - type: s2 - - id: mag_acc - type: u2 -enums: - gnss_type: - 0: gps - 1: sbas - 2: galileo - 3: beidou - 4: imes - 5: qzss - 6: glonass diff --git a/system/ubloxd/ubx.py b/system/ubloxd/ubx.py new file mode 100644 index 0000000000..857498ebf1 --- /dev/null +++ b/system/ubloxd/ubx.py @@ -0,0 +1,180 @@ +""" +UBX protocol parser +""" + +from enum import IntEnum +from typing import Annotated + +from openpilot.system.ubloxd import binary_struct as bs + + +class GnssType(IntEnum): + gps = 0 + sbas = 1 + galileo = 2 + beidou = 3 + imes = 4 + qzss = 5 + glonass = 6 + + +class Ubx(bs.BinaryStruct): + GnssType = GnssType + + class RxmRawx(bs.BinaryStruct): + class Measurement(bs.BinaryStruct): + pr_mes: Annotated[float, bs.f64] + cp_mes: Annotated[float, bs.f64] + do_mes: Annotated[float, bs.f32] + gnss_id: Annotated[GnssType | int, bs.enum(bs.u8, GnssType)] + sv_id: Annotated[int, bs.u8] + reserved2: Annotated[bytes, bs.bytes_field(1)] + freq_id: Annotated[int, bs.u8] + lock_time: Annotated[int, bs.u16] + cno: Annotated[int, bs.u8] + pr_stdev: Annotated[int, bs.u8] + cp_stdev: Annotated[int, bs.u8] + do_stdev: Annotated[int, bs.u8] + trk_stat: Annotated[int, bs.u8] + reserved3: Annotated[bytes, bs.bytes_field(1)] + + rcv_tow: Annotated[float, bs.f64] + week: Annotated[int, bs.u16] + leap_s: Annotated[int, bs.s8] + num_meas: Annotated[int, bs.u8] + rec_stat: Annotated[int, bs.u8] + reserved1: Annotated[bytes, bs.bytes_field(3)] + meas: Annotated[list[Measurement], bs.array(Measurement, count_field='num_meas')] + + class RxmSfrbx(bs.BinaryStruct): + gnss_id: Annotated[GnssType | int, bs.enum(bs.u8, GnssType)] + sv_id: Annotated[int, bs.u8] + reserved1: Annotated[bytes, bs.bytes_field(1)] + freq_id: Annotated[int, bs.u8] + num_words: Annotated[int, bs.u8] + reserved2: Annotated[bytes, bs.bytes_field(1)] + version: Annotated[int, bs.u8] + reserved3: Annotated[bytes, bs.bytes_field(1)] + body: Annotated[list[int], bs.array(bs.u32, count_field='num_words')] + + class NavSat(bs.BinaryStruct): + class Nav(bs.BinaryStruct): + gnss_id: Annotated[GnssType | int, bs.enum(bs.u8, GnssType)] + sv_id: Annotated[int, bs.u8] + cno: Annotated[int, bs.u8] + elev: Annotated[int, bs.s8] + azim: Annotated[int, bs.s16] + pr_res: Annotated[int, bs.s16] + flags: Annotated[int, bs.u32] + + itow: Annotated[int, bs.u32] + version: Annotated[int, bs.u8] + num_svs: Annotated[int, bs.u8] + reserved: Annotated[bytes, bs.bytes_field(2)] + svs: Annotated[list[Nav], bs.array(Nav, count_field='num_svs')] + + class NavPvt(bs.BinaryStruct): + i_tow: Annotated[int, bs.u32] + year: Annotated[int, bs.u16] + month: Annotated[int, bs.u8] + day: Annotated[int, bs.u8] + hour: Annotated[int, bs.u8] + min: Annotated[int, bs.u8] + sec: Annotated[int, bs.u8] + valid: Annotated[int, bs.u8] + t_acc: Annotated[int, bs.u32] + nano: Annotated[int, bs.s32] + fix_type: Annotated[int, bs.u8] + flags: Annotated[int, bs.u8] + flags2: Annotated[int, bs.u8] + num_sv: Annotated[int, bs.u8] + lon: Annotated[int, bs.s32] + lat: Annotated[int, bs.s32] + height: Annotated[int, bs.s32] + h_msl: Annotated[int, bs.s32] + h_acc: Annotated[int, bs.u32] + v_acc: Annotated[int, bs.u32] + vel_n: Annotated[int, bs.s32] + vel_e: Annotated[int, bs.s32] + vel_d: Annotated[int, bs.s32] + g_speed: Annotated[int, bs.s32] + head_mot: Annotated[int, bs.s32] + s_acc: Annotated[int, bs.s32] + head_acc: Annotated[int, bs.u32] + p_dop: Annotated[int, bs.u16] + flags3: Annotated[int, bs.u8] + reserved1: Annotated[bytes, bs.bytes_field(5)] + head_veh: Annotated[int, bs.s32] + mag_dec: Annotated[int, bs.s16] + mag_acc: Annotated[int, bs.u16] + + class MonHw2(bs.BinaryStruct): + class ConfigSource(IntEnum): + flash = 102 + otp = 111 + config_pins = 112 + rom = 113 + + ofs_i: Annotated[int, bs.s8] + mag_i: Annotated[int, bs.u8] + ofs_q: Annotated[int, bs.s8] + mag_q: Annotated[int, bs.u8] + cfg_source: Annotated[ConfigSource | int, bs.enum(bs.u8, ConfigSource)] + reserved1: Annotated[bytes, bs.bytes_field(3)] + low_lev_cfg: Annotated[int, bs.u32] + reserved2: Annotated[bytes, bs.bytes_field(8)] + post_status: Annotated[int, bs.u32] + reserved3: Annotated[bytes, bs.bytes_field(4)] + + class MonHw(bs.BinaryStruct): + class AntennaStatus(IntEnum): + init = 0 + dontknow = 1 + ok = 2 + short = 3 + open = 4 + + class AntennaPower(IntEnum): + false = 0 + true = 1 + dontknow = 2 + + pin_sel: Annotated[int, bs.u32] + pin_bank: Annotated[int, bs.u32] + pin_dir: Annotated[int, bs.u32] + pin_val: Annotated[int, bs.u32] + noise_per_ms: Annotated[int, bs.u16] + agc_cnt: Annotated[int, bs.u16] + a_status: Annotated[AntennaStatus | int, bs.enum(bs.u8, AntennaStatus)] + a_power: Annotated[AntennaPower | int, bs.enum(bs.u8, AntennaPower)] + flags: Annotated[int, bs.u8] + reserved1: Annotated[bytes, bs.bytes_field(1)] + used_mask: Annotated[int, bs.u32] + vp: Annotated[bytes, bs.bytes_field(17)] + jam_ind: Annotated[int, bs.u8] + reserved2: Annotated[bytes, bs.bytes_field(2)] + pin_irq: Annotated[int, bs.u32] + pull_h: Annotated[int, bs.u32] + pull_l: Annotated[int, bs.u32] + + magic: Annotated[bytes, bs.const(bs.bytes_field(2), b"\xb5\x62")] + msg_type: Annotated[int, bs.u16be] + length: Annotated[int, bs.u16] + body: Annotated[ + object, + bs.substream( + 'length', + bs.switch( + 'msg_type', + { + 0x0107: NavPvt, + 0x0213: RxmSfrbx, + 0x0215: RxmRawx, + 0x0A09: MonHw, + 0x0A0B: MonHw2, + 0x0135: NavSat, + }, + ), + ), + ] + checksum: Annotated[int, bs.u16] diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index fcc5ddb727..9e902508b8 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -1,6 +1,7 @@ import atexit import cffi import os +import queue import time import signal import sys @@ -42,6 +43,9 @@ PROFILE_RENDER = int(os.getenv("PROFILE_RENDER", "0")) PROFILE_STATS = int(os.getenv("PROFILE_STATS", "100")) # Number of functions to show in profile output RECORD = os.getenv("RECORD") == "1" RECORD_OUTPUT = str(Path(os.getenv("RECORD_OUTPUT", "output")).with_suffix(".mp4")) +RECORD_BITRATE = os.getenv("RECORD_BITRATE", "") # Target bitrate e.g. "2000k" +RECORD_SPEED = int(os.getenv("RECORD_SPEED", "1")) # Speed multiplier +OFFSCREEN = os.getenv("OFFSCREEN") == "1" # Disable FPS limiting for fast offline rendering GL_VERSION = """ #version 300 es @@ -216,6 +220,9 @@ class GuiApplication(GuiApplicationExt): self._render_texture: rl.RenderTexture | None = None self._burn_in_shader: rl.Shader | None = None self._ffmpeg_proc: subprocess.Popen | None = None + self._ffmpeg_queue: queue.Queue | None = None + self._ffmpeg_thread: threading.Thread | None = None + self._ffmpeg_stop_event: threading.Event | None = None self._textures: dict[str, rl.Texture] = {} self._target_fps: int = _DEFAULT_FPS self._last_fps_log_time: float = time.monotonic() @@ -282,25 +289,36 @@ class GuiApplication(GuiApplicationExt): rl.set_texture_filter(self._render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) if RECORD: + output_fps = fps * RECORD_SPEED ffmpeg_args = [ 'ffmpeg', '-v', 'warning', # Reduce ffmpeg log spam - '-stats', # Show encoding progress + '-nostats', # Suppress encoding progress '-f', 'rawvideo', # Input format '-pix_fmt', 'rgba', # Input pixel format '-s', f'{self._width}x{self._height}', # Input resolution '-r', str(fps), # Input frame rate '-i', 'pipe:0', # Input from stdin - '-vf', 'vflip,format=yuv420p', # Flip vertically and convert rgba to yuv420p - '-c:v', 'libx264', # Video codec - '-preset', 'ultrafast', # Encoding speed + '-vf', 'vflip,format=yuv420p', # Flip vertically and convert to yuv420p + '-r', str(output_fps), # Output frame rate (for speed multiplier) + '-c:v', 'libx264', + '-preset', 'ultrafast', + ] + if RECORD_BITRATE: + ffmpeg_args += ['-b:v', RECORD_BITRATE, '-maxrate', RECORD_BITRATE, '-bufsize', RECORD_BITRATE] + ffmpeg_args += [ '-y', # Overwrite existing file '-f', 'mp4', # Output format RECORD_OUTPUT, # Output file path ] self._ffmpeg_proc = subprocess.Popen(ffmpeg_args, stdin=subprocess.PIPE) + self._ffmpeg_queue = queue.Queue(maxsize=60) # Buffer up to 60 frames + self._ffmpeg_stop_event = threading.Event() + self._ffmpeg_thread = threading.Thread(target=self._ffmpeg_writer_thread, daemon=True) + self._ffmpeg_thread.start() - rl.set_target_fps(fps) + # OFFSCREEN disables FPS limiting for fast offline rendering (e.g. clips) + rl.set_target_fps(0 if OFFSCREEN else fps) self._target_fps = fps self._set_styles() @@ -342,6 +360,21 @@ class GuiApplication(GuiApplicationExt): print(f"{green}UI window ready in {elapsed_ms:.1f} ms{reset}") sys.exit(0) + def _ffmpeg_writer_thread(self): + """Background thread that writes frames to ffmpeg.""" + while True: + try: + data = self._ffmpeg_queue.get(timeout=1.0) + if data is None: # Sentinel to stop + break + self._ffmpeg_proc.stdin.write(data) + except queue.Empty: + if self._ffmpeg_stop_event.is_set(): + break + continue + except Exception: + break + def set_modal_overlay(self, overlay, callback: Callable | None = None): if self._modal_overlay.overlay is not None: if hasattr(self._modal_overlay.overlay, 'hide_event'): @@ -414,11 +447,17 @@ class GuiApplication(GuiApplicationExt): return texture def close_ffmpeg(self): + if self._ffmpeg_thread is not None: + # Signal thread to stop, send sentinel, then wait for it to drain + self._ffmpeg_stop_event.set() + self._ffmpeg_queue.put(None) + self._ffmpeg_thread.join(timeout=30) + if self._ffmpeg_proc is not None: self._ffmpeg_proc.stdin.flush() self._ffmpeg_proc.stdin.close() try: - self._ffmpeg_proc.wait(timeout=5) + self._ffmpeg_proc.wait(timeout=30) except subprocess.TimeoutExpired: self._ffmpeg_proc.terminate() self._ffmpeg_proc.wait() @@ -533,8 +572,7 @@ class GuiApplication(GuiApplicationExt): image = rl.load_image_from_texture(self._render_texture.texture) data_size = image.width * image.height * 4 data = bytes(rl.ffi.buffer(image.data, data_size)) - self._ffmpeg_proc.stdin.write(data) - self._ffmpeg_proc.stdin.flush() + self._ffmpeg_queue.put(data) # Async write via background thread rl.unload_image(image) self._monitor_fps() diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index adc3f125c8..75f6ec6d90 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -49,7 +49,7 @@ exec ./launch_openpilot.sh class NetworkConnectivityMonitor: - def __init__(self, should_check: Callable[[], bool] | None = None, check_interval: float = 0.5): + def __init__(self, should_check: Callable[[], bool] | None = None, check_interval: float = 1.0): self.network_connected = threading.Event() self.wifi_connected = threading.Event() self._should_check = should_check or (lambda: True) @@ -78,7 +78,7 @@ class NetworkConnectivityMonitor: if self._should_check(): try: request = urllib.request.Request(OPENPILOT_URL, method="HEAD") - urllib.request.urlopen(request, timeout=0.5) + urllib.request.urlopen(request, timeout=1.0) self.network_connected.set() if HARDWARE.get_network_type() == NetworkType.wifi: self.wifi_connected.set() @@ -528,9 +528,8 @@ class Setup(Widget): self.download_thread = None self._wifi_manager = WifiManager() self._wifi_manager.set_active(True) - self._network_monitor = NetworkConnectivityMonitor( - lambda: self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE) - ) + self._network_monitor = NetworkConnectivityMonitor() + self._network_monitor.start() self._prev_has_internet = False gui_app.set_modal_overlay_tick(self._modal_overlay_tick) @@ -569,10 +568,8 @@ class Setup(Widget): if self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE): self._network_setup_page.show_event() self._network_monitor.reset() - self._network_monitor.start() else: self._network_setup_page.hide_event() - self._network_monitor.stop() def _render(self, rect: rl.Rectangle): if self.state == SetupState.GETTING_STARTED: @@ -618,7 +615,6 @@ class Setup(Widget): self._set_state(SetupState.SOFTWARE_SELECTION) def _network_setup_continue_button_callback(self): - self._network_monitor.stop() if self.state == SetupState.NETWORK_SETUP: self.download(OPENPILOT_URL) elif self.state == SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE: @@ -628,10 +624,10 @@ class Setup(Widget): self._network_monitor.stop() def render_network_setup(self, rect: rl.Rectangle): - self._network_setup_page.render(rect) has_internet = self._network_monitor.network_connected.is_set() self._prev_has_internet = has_internet self._network_setup_page.set_has_internet(has_internet) + self._network_setup_page.render(rect) def render_downloading(self, rect: rl.Rectangle): self._downloading_page.set_progress(self.download_progress) diff --git a/system/ui/widgets/mici_keyboard.py b/system/ui/widgets/mici_keyboard.py index 7459dc5731..6d2e08e053 100644 --- a/system/ui/widgets/mici_keyboard.py +++ b/system/ui/widgets/mici_keyboard.py @@ -38,10 +38,10 @@ def fast_euclidean_distance(dx, dy): class Key(Widget): - def __init__(self, char: str): + def __init__(self, char: str, font_weight: FontWeight = FontWeight.SEMI_BOLD): super().__init__() self.char = char - self._font = gui_app.font(FontWeight.SEMI_BOLD) + self._font = gui_app.font(font_weight) self._x_filter = BounceFilter(0.0, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) self._y_filter = BounceFilter(0.0, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) self._size_filter = BounceFilter(CHAR_FONT_SIZE, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) @@ -53,20 +53,23 @@ class Key(Widget): self.original_position = rl.Vector2(0, 0) def set_position(self, x: float, y: float, smooth: bool = True): - # TODO: swipe up from NavWidget has the keys lag behind other elements a bit + # Smooth keys within parent rect + base_y = self._parent_rect.y if self._parent_rect else 0.0 + local_y = y - base_y + if not self._position_initialized: self._x_filter.x = x - self._y_filter.x = y + self._y_filter.x = local_y # keep track of original position so dragging around feels consistent. also move touch area down a bit self.original_position = rl.Vector2(x, y + KEY_TOUCH_AREA_OFFSET) self._position_initialized = True if not smooth: self._x_filter.x = x - self._y_filter.x = y + self._y_filter.x = local_y self._rect.x = self._x_filter.update(x) - self._rect.y = self._y_filter.update(y) + self._rect.y = base_y + self._y_filter.update(local_y) def set_alpha(self, alpha: float): self._alpha_filter.update(alpha) @@ -97,7 +100,7 @@ class Key(Widget): class SmallKey(Key): def __init__(self, chars: str): - super().__init__(chars) + super().__init__(chars, FontWeight.BOLD) self._size_filter.x = NUMBER_LAYER_SWITCH_FONT_SIZE def set_font_size(self, size: float): @@ -105,13 +108,15 @@ class SmallKey(Key): class IconKey(Key): - def __init__(self, icon: str, vertical_align: str = "center", char: str = ""): + def __init__(self, icon: str, vertical_align: str = "center", char: str = "", icon_size: tuple[int, int] = (38, 38)): super().__init__(char) - self._icon = gui_app.texture(icon, 38, 38) + self._icon_size = icon_size + self._icon = gui_app.texture(icon, *icon_size) self._vertical_align = vertical_align - def set_icon(self, icon: str): - self._icon = gui_app.texture(icon, 38, 38) + def set_icon(self, icon: str, icon_size: tuple[int, int] | None = None): + size = icon_size if icon_size is not None else self._icon_size + self._icon = gui_app.texture(icon, *size) def _render(self, _): scale = np.interp(self._size_filter.x, [CHAR_FONT_SIZE, CHAR_NEAR_FONT_SIZE], [1, 1.5]) @@ -167,8 +172,8 @@ class MiciKeyboard(Widget): self._super_special_keys = [[Key(char) for char in row] for row in super_special_chars] # control keys - self._space_key = IconKey("icons_mici/settings/keyboard/space.png", char=" ", vertical_align="bottom") - self._caps_key = IconKey("icons_mici/settings/keyboard/caps_lower.png") + self._space_key = IconKey("icons_mici/settings/keyboard/space.png", char=" ", vertical_align="bottom", icon_size=(43, 14)) + self._caps_key = IconKey("icons_mici/settings/keyboard/caps_lower.png", icon_size=(38, 33)) # these two are in different places on some layouts self._123_key, self._123_key2 = SmallKey("123"), SmallKey("123") self._abc_key = SmallKey("abc") @@ -269,14 +274,14 @@ class MiciKeyboard(Widget): self._set_keys(self._upper_keys if cycle else self._lower_keys) if not cycle: self._caps_state = CapsState.LOWER - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lower.png") + self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lower.png", icon_size=(38, 33)) else: if self._caps_state == CapsState.LOWER: self._caps_state = CapsState.UPPER - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_upper.png") + self._caps_key.set_icon("icons_mici/settings/keyboard/caps_upper.png", icon_size=(38, 33)) elif self._caps_state == CapsState.UPPER: self._caps_state = CapsState.LOCK - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lock.png") + self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lock.png", icon_size=(39, 38)) else: self._set_uppercase(False) @@ -365,6 +370,7 @@ class MiciKeyboard(Widget): key.set_font_size(font_size) # TODO: I like the push amount, so we should clip the pos inside the keyboard rect + key.set_parent_rect(self._rect) key.set_position(key_x, key_y) def _render(self, _): diff --git a/third_party/acados/acados_template/acados_ocp.py b/third_party/acados/acados_template/acados_ocp.py index ec02822ceb..d6236e1f6e 100644 --- a/third_party/acados/acados_template/acados_ocp.py +++ b/third_party/acados/acados_template/acados_ocp.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_ocp_solver.py b/third_party/acados/acados_template/acados_ocp_solver.py index ffc9cf4b0e..229bdf6039 100644 --- a/third_party/acados/acados_template/acados_ocp_solver.py +++ b/third_party/acados/acados_template/acados_ocp_solver.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_ocp_solver_pyx.pyx b/third_party/acados/acados_template/acados_ocp_solver_pyx.pyx index acd7f02d0a..bc03ba086f 100644 --- a/third_party/acados/acados_template/acados_ocp_solver_pyx.pyx +++ b/third_party/acados/acados_template/acados_ocp_solver_pyx.pyx @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_sim.py b/third_party/acados/acados_template/acados_sim.py index c0d6937a49..7faa49fb12 100644 --- a/third_party/acados/acados_template/acados_sim.py +++ b/third_party/acados/acados_template/acados_sim.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_sim_solver.py b/third_party/acados/acados_template/acados_sim_solver.py index 612f439eaf..de5ee10709 100644 --- a/third_party/acados/acados_template/acados_sim_solver.py +++ b/third_party/acados/acados_template/acados_sim_solver.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_sim_solver_common.pxd b/third_party/acados/acados_template/acados_sim_solver_common.pxd index cc6a58efd7..7c20a6d24d 100644 --- a/third_party/acados/acados_template/acados_sim_solver_common.pxd +++ b/third_party/acados/acados_template/acados_sim_solver_common.pxd @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_sim_solver_pyx.pyx b/third_party/acados/acados_template/acados_sim_solver_pyx.pyx index be400addc7..01964fd7a0 100644 --- a/third_party/acados/acados_template/acados_sim_solver_pyx.pyx +++ b/third_party/acados/acados_template/acados_sim_solver_pyx.pyx @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/acados_solver_common.pxd b/third_party/acados/acados_template/acados_solver_common.pxd index c6d59d40a5..75d021626f 100644 --- a/third_party/acados/acados_template/acados_solver_common.pxd +++ b/third_party/acados/acados_template/acados_solver_common.pxd @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/acados_template/builders.py b/third_party/acados/acados_template/builders.py index 6f21bfe8cd..8acc05b528 100644 --- a/third_party/acados/acados_template/builders.py +++ b/third_party/acados/acados_template/builders.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright 2019 Gianluca Frison, Dimitris Kouzoupis, Robin Verschueren, # Andrea Zanelli, Niels van Duijkeren, Jonathan Frey, Tommaso Sartor, diff --git a/third_party/acados/acados_template/gnsf/__init__.py b/third_party/acados/acados_template/gnsf/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/third_party/acados/acados_template/utils.py b/third_party/acados/acados_template/utils.py index d6f6c02f84..f27617fa30 100644 --- a/third_party/acados/acados_template/utils.py +++ b/third_party/acados/acados_template/utils.py @@ -1,4 +1,3 @@ -# -*- coding: future_fstrings -*- # # Copyright (c) The acados authors. # diff --git a/third_party/acados/build.sh b/third_party/acados/build.sh index b45c167b16..2b803ef6b2 100755 --- a/third_party/acados/build.sh +++ b/third_party/acados/build.sh @@ -44,6 +44,12 @@ cp -r $DIR/acados_repo/lib $INSTALL_DIR cp -r $DIR/acados_repo/interfaces/acados_template/acados_template $DIR/ #pip3 install -e $DIR/acados/interfaces/acados_template +# skip macOS - sed is different :/ +if [[ "$OSTYPE" != "darwin"* ]]; then + # strip future_fstrings to avoid having to install the compatibility package + find $DIR/acados_template/ -type f -exec sed -i '/future.fstrings/d' {} + +fi + # build tera cd $DIR/acados_repo/interfaces/acados_template/tera_renderer/ if [[ "$OSTYPE" == "darwin"* ]]; then diff --git a/tools/clip/run.py b/tools/clip/run.py index 9045a4381b..5fb693f30a 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -1,310 +1,340 @@ #!/usr/bin/env python3 - -import logging import os -import platform -import shutil import sys import time -from argparse import ArgumentParser, ArgumentTypeError -from collections.abc import Sequence +import logging +import subprocess +import threading +import queue +import multiprocessing +import itertools +import numpy as np +import tqdm +from argparse import ArgumentParser from pathlib import Path -from random import randint -from subprocess import Popen -from typing import Literal +from concurrent.futures import ThreadPoolExecutor, as_completed -from cereal.messaging import SubMaster -from openpilot.common.basedir import BASEDIR -from openpilot.common.params import Params, UnknownKeyName -from openpilot.common.prefix import OpenpilotPrefix -from openpilot.common.utils import managed_proc from openpilot.tools.lib.route import Route from openpilot.tools.lib.logreader import LogReader +from openpilot.tools.lib.filereader import FileReader +from openpilot.tools.lib.framereader import FrameReader, ffprobe +from openpilot.selfdrive.test.process_replay.migration import migrate_all +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.common.utils import Timer +from msgq.visionipc import VisionIpcServer, VisionStreamType -DEFAULT_OUTPUT = 'output.mp4' -DEMO_START = 90 -DEMO_END = 105 -DEMO_ROUTE = 'a2a0ccea32023010/2023-07-27--13-01-19' FRAMERATE = 20 -PIXEL_DEPTH = '24' -RESOLUTION = '2160x1080' -SECONDS_TO_WARM = 2 -PROC_WAIT_SECONDS = 30*10 +DEMO_ROUTE, DEMO_START, DEMO_END = 'a2a0ccea32023010/2023-07-27--13-01-19', 90, 105 -OPENPILOT_FONT = str(Path(BASEDIR, 'selfdrive/assets/fonts/Inter-Regular.ttf').resolve()) -REPLAY = str(Path(BASEDIR, 'tools/replay/replay').resolve()) -UI = str(Path(BASEDIR, 'selfdrive/ui/ui').resolve()) - -logger = logging.getLogger('clip.py') +logger = logging.getLogger('clip') -def check_for_failure(procs: list[Popen]): - for proc in procs: - exit_code = proc.poll() - if exit_code is not None and exit_code != 0: - cmd = str(proc.args) - if isinstance(proc.args, str): - cmd = proc.args - elif isinstance(proc.args, Sequence): - cmd = str(proc.args[0]) - msg = f'{cmd} failed, exit code {exit_code}' - logger.error(msg) - stdout, stderr = proc.communicate() - if stdout: - logger.error(stdout.decode()) - if stderr: - logger.error(stderr.decode()) - raise ChildProcessError(msg) - - -def escape_ffmpeg_text(value: str): - special_chars = {',': '\\,', ':': '\\:', '=': '\\=', '[': '\\[', ']': '\\]'} - value = value.replace('\\', '\\\\\\\\\\\\\\\\') - for char, escaped in special_chars.items(): - value = value.replace(char, escaped) - return value - - -def get_logreader(route: Route): - return LogReader(route.qlog_paths()[0] if len(route.qlog_paths()) else route.name.canonical_name) - - -def get_meta_text(lr: LogReader, route: Route): - init_data = lr.first('initData') - car_params = lr.first('carParams') - origin_parts = init_data.gitRemote.split('/') - origin = origin_parts[3] if len(origin_parts) > 3 else 'unknown' - return ', '.join([ - f"openpilot v{init_data.version}", - f"route: {route.name.canonical_name}", - f"car: {car_params.carFingerprint}", - f"origin: {origin}", - f"branch: {init_data.gitBranch}", - f"commit: {init_data.gitCommit[:7]}", - f"modified: {str(init_data.dirty).lower()}", - ]) - - -def parse_args(parser: ArgumentParser): +def parse_args(): + parser = ArgumentParser(description="Direct clip renderer") + parser.add_argument("route", nargs="?", help="Route ID (dongle/route or dongle/route/start/end)") + parser.add_argument("-s", "--start", type=int, help="Start time in seconds") + parser.add_argument("-e", "--end", type=int, help="End time in seconds") + parser.add_argument("-o", "--output", default="output.mp4", help="Output file path") + parser.add_argument("-d", "--data-dir", help="Local directory with route data") + parser.add_argument("-t", "--title", help="Title overlay text") + parser.add_argument("-f", "--file-size", type=float, default=9.0, help="Target file size in MB") + parser.add_argument("-x", "--speed", type=int, default=1, help="Speed multiplier") + parser.add_argument("--demo", action="store_true", help="Use demo route with default timing") + parser.add_argument("--big", action="store_true", default=True, help="Use big UI (2160x1080)") + parser.add_argument("--qcam", action="store_true", help="Use qcamera instead of fcamera") + parser.add_argument("--windowed", action="store_true", help="Show window") + parser.add_argument("--no-metadata", action="store_true", help="Disable metadata overlay") + parser.add_argument("--no-time-overlay", action="store_true", help="Disable time overlay") args = parser.parse_args() + if args.demo: - args.route = DEMO_ROUTE - if args.start is None or args.end is None: - args.start = DEMO_START - args.end = DEMO_END - elif args.route.count('/') == 1: - if args.start is None or args.end is None: - parser.error('must provide both start and end if timing is not in the route ID') - elif args.route.count('/') == 3: - if args.start is not None or args.end is not None: - parser.error('don\'t provide timing when including it in the route ID') + args.route, args.start, args.end = args.route or DEMO_ROUTE, args.start or DEMO_START, args.end or DEMO_END + elif not args.route: + parser.error("route is required (or use --demo)") + + if args.route and args.route.count('/') == 3: parts = args.route.split('/') - args.route = '/'.join(parts[:2]) - args.start = int(parts[2]) - args.end = int(parts[3]) + args.route, args.start, args.end = '/'.join(parts[:2]), args.start or int(parts[2]), args.end or int(parts[3]) + + if args.start is None or args.end is None: + parser.error("--start and --end are required") if args.end <= args.start: - parser.error(f'end ({args.end}) must be greater than start ({args.start})') - if args.start < SECONDS_TO_WARM: - parser.error(f'start must be greater than {SECONDS_TO_WARM}s to allow the UI time to warm up') - - try: - args.route = Route(args.route, data_dir=args.data_dir) - except Exception as e: - parser.error(f'failed to get route: {e}') - - # FIXME: length isn't exactly max segment seconds, simplify to replay exiting at end of data - length = round(args.route.max_seg_number * 60) - if args.start >= length: - parser.error(f'start ({args.start}s) cannot be after end of route ({length}s)') - if args.end > length: - parser.error(f'end ({args.end}s) cannot be after end of route ({length}s)') - + parser.error(f"end ({args.end}) must be greater than start ({args.start})") return args -def populate_car_params(lr: LogReader): - init_data = lr.first('initData') - assert init_data is not None +def setup_env(output_path: str, big: bool = False, speed: int = 1, target_mb: float = 0, duration: int = 0): + os.environ.update({"RECORD": "1", "OFFSCREEN": "1", "RECORD_OUTPUT": str(Path(output_path).with_suffix(".mp4"))}) + if speed > 1: + os.environ["RECORD_SPEED"] = str(speed) + if target_mb > 0 and duration > 0: + os.environ["RECORD_BITRATE"] = f"{int(target_mb * 8 * 1024 / (duration / speed))}k" + if big: + os.environ["BIG"] = "1" + + +def _download_segment(path: str) -> bytes: + with FileReader(path) as f: + return bytes(f.read()) + + +def _parse_and_chunk_segment(args: tuple) -> list[dict]: + raw_data, fps = args + from openpilot.tools.lib.logreader import _LogFileReader + messages = migrate_all(list(_LogFileReader("", dat=raw_data, sort_by_time=True))) + if not messages: + return [] + + dt_ns, chunks, current, next_time = 1e9 / fps, [], {}, messages[0].logMonoTime + 1e9 / fps # type: ignore[var-annotated] + for msg in messages: + if msg.logMonoTime >= next_time: + chunks.append(current) + current, next_time = {}, next_time + dt_ns * ((msg.logMonoTime - next_time) // dt_ns + 1) + current[msg.which()] = msg + return chunks + [current] if current else chunks + + +def load_logs_parallel(log_paths: list[str], fps: int = 20) -> list[dict]: + num_workers = min(16, len(log_paths), (multiprocessing.cpu_count() or 1)) + logger.info(f"Downloading {len(log_paths)} segments with {num_workers} workers...") + + with ThreadPoolExecutor(max_workers=num_workers) as pool: + futures = {pool.submit(_download_segment, path): idx for idx, path in enumerate(log_paths)} + raw_data = {futures[f]: f.result() for f in as_completed(futures)} + + logger.info("Parsing and chunking segments...") + with multiprocessing.Pool(num_workers) as pool: + return list(itertools.chain.from_iterable(pool.map(_parse_and_chunk_segment, [(raw_data[i], fps) for i in range(len(log_paths))]))) + + +def patch_submaster(message_chunks, ui_state): + # Reset started_frame so alerts render correctly (recv_frame must be >= started_frame) + ui_state.started_frame = 0 + ui_state.started_time = time.monotonic() + + def mock_update(timeout=None): + sm, t = ui_state.sm, time.monotonic() + sm.updated = dict.fromkeys(sm.services, False) + if sm.frame < len(message_chunks): + for svc, msg in message_chunks[sm.frame].items(): + if svc in sm.data: + sm.seen[svc] = sm.updated[svc] = sm.alive[svc] = sm.valid[svc] = True + sm.data[svc] = getattr(msg.as_builder(), svc) + sm.logMonoTime[svc], sm.recv_time[svc], sm.recv_frame[svc] = msg.logMonoTime, t, sm.frame + sm.frame += 1 + ui_state.sm.update = mock_update + + +def get_frame_dimensions(camera_path: str) -> tuple[int, int]: + """Get frame dimensions from a video file using ffprobe.""" + probe = ffprobe(camera_path) + stream = probe["streams"][0] + return stream["width"], stream["height"] + + +def iter_segment_frames(camera_paths, start_time, end_time, fps=20, use_qcam=False, frame_size: tuple[int, int] | None = None): + frames_per_seg = fps * 60 + start_frame, end_frame = int(start_time * fps), int(end_time * fps) + current_seg: int = -1 + seg_frames: FrameReader | np.ndarray | None = None + + for global_idx in range(start_frame, end_frame): + seg_idx, local_idx = global_idx // frames_per_seg, global_idx % frames_per_seg + + if seg_idx != current_seg: + current_seg = seg_idx + path = camera_paths[seg_idx] if seg_idx < len(camera_paths) else None + if not path: + raise RuntimeError(f"No camera file for segment {seg_idx}") + + if use_qcam: + w, h = frame_size or get_frame_dimensions(path) + with FileReader(path) as f: + result = subprocess.run(["ffmpeg", "-v", "quiet", "-i", "-", "-f", "rawvideo", "-pix_fmt", "nv12", "-"], + input=f.read(), capture_output=True) + if result.returncode != 0: + raise RuntimeError(f"ffmpeg failed: {result.stderr.decode()}") + seg_frames = np.frombuffer(result.stdout, dtype=np.uint8).reshape(-1, w * h * 3 // 2) + else: + seg_frames = FrameReader(path, pix_fmt="nv12") + + assert seg_frames is not None + frame = seg_frames[local_idx] if use_qcam else seg_frames.get(local_idx) # type: ignore[index, union-attr] + yield global_idx, frame + + +class FrameQueue: + def __init__(self, camera_paths, start_time, end_time, fps=20, prefetch_count=60, use_qcam=False): + # Probe first valid camera file for dimensions + first_path = next((p for p in camera_paths if p), None) + if not first_path: + raise RuntimeError("No valid camera paths") + self.frame_w, self.frame_h = get_frame_dimensions(first_path) + + self._queue, self._stop, self._error = queue.Queue(maxsize=prefetch_count), threading.Event(), None + self._thread = threading.Thread(target=self._worker, + args=(camera_paths, start_time, end_time, fps, use_qcam, (self.frame_w, self.frame_h)), daemon=True) + self._thread.start() + + def _worker(self, camera_paths, start_time, end_time, fps, use_qcam, frame_size): + try: + for idx, data in iter_segment_frames(camera_paths, start_time, end_time, fps, use_qcam, frame_size): + if self._stop.is_set(): + break + self._queue.put((idx, data.tobytes())) + except Exception as e: + logger.exception("Decode error") + self._error = e + finally: + self._queue.put(None) + + def get(self, timeout=60.0): + if self._error: + raise self._error + result = self._queue.get(timeout=timeout) + if result is None: + raise StopIteration("No more frames") + return result + + def stop(self): + self._stop.set() + while not self._queue.empty(): + try: + self._queue.get_nowait() + except queue.Empty: + break + self._thread.join(timeout=2.0) + + +def load_route_metadata(route): + from openpilot.common.params import Params, UnknownKeyName + lr = LogReader(route.log_paths()[0]) + init_data, car_params = lr.first('initData'), lr.first('carParams') params = Params() - entries = init_data.params.entries - for cp in entries: - key, value = cp.key, cp.value + for entry in init_data.params.entries: try: - params.put(key, params.cpp2python(key, value)) + params.put(entry.key, params.cpp2python(entry.key, entry.value)) except UnknownKeyName: - # forks of openpilot may have other Params keys configured. ignore these - logger.warning(f"unknown Params key '{key}', skipping") - logger.debug('persisted CarParams') + pass + + origin = init_data.gitRemote.split('/')[3] if len(init_data.gitRemote.split('/')) > 3 else 'unknown' + return { + 'version': init_data.version, 'route': route.name.canonical_name, + 'car': car_params.carFingerprint if car_params else 'unknown', 'origin': origin, + 'branch': init_data.gitBranch, 'commit': init_data.gitCommit[:7], 'modified': str(init_data.dirty).lower(), + } -def validate_env(parser: ArgumentParser): - if platform.system() not in ['Linux']: - parser.exit(1, f'clip.py: error: {platform.system()} is not a supported operating system\n') - for proc in ['Xvfb', 'ffmpeg']: - if shutil.which(proc) is None: - parser.exit(1, f'clip.py: error: missing {proc} command, is it installed?\n') - for proc in [REPLAY, UI]: - if shutil.which(proc) is None: - parser.exit(1, f'clip.py: error: missing {proc} command, did you build openpilot yet?\n') +def draw_text_box(rl, text, x, y, size, gui_app, font, font_scale, color=None, center=False): + box_color, text_color = rl.Color(0, 0, 0, 85), color or rl.WHITE + # measure_text_ex is NOT auto-scaled, so multiply by font_scale + # draw_text_ex IS auto-scaled, so pass size directly + text_size = rl.measure_text_ex(font, text, size * font_scale, 0) + text_width, text_height = int(text_size.x), int(text_size.y) + if center: + x = (gui_app.width - text_width) // 2 + rl.draw_rectangle(x - 8, y - 4, text_width + 16, text_height + 8, box_color) + rl.draw_text_ex(font, text, rl.Vector2(x, y), size, 0, text_color) -def validate_output_file(output_file: str): - if not output_file.endswith('.mp4'): - raise ArgumentTypeError('output must be an mp4') - return output_file +def render_overlays(rl, gui_app, font, font_scale, metadata, title, start_time, frame_idx, show_metadata, show_time): + if show_metadata and metadata and frame_idx < FRAMERATE * 5: + m = metadata + text = ", ".join([f"openpilot v{m['version']}", f"route: {m['route']}", f"car: {m['car']}", f"origin: {m['origin']}", + f"branch: {m['branch']}", f"commit: {m['commit']}", f"modified: {m['modified']}"]) + # Truncate if too wide (leave 20px margin on each side) + max_width = gui_app.width - 40 + while rl.measure_text_ex(font, text, 15 * font_scale, 0).x > max_width and len(text) > 20: + text = text[:-4] + "..." + draw_text_box(rl, text, 0, 8, 15, gui_app, font, font_scale, center=True) - -def validate_route(route: str): - if route.count('/') not in (1, 3): - raise ArgumentTypeError(f'route must include or exclude timing, example: {DEMO_ROUTE}') - return route - - -def validate_title(title: str): - if len(title) > 80: - raise ArgumentTypeError('title must be no longer than 80 chars') - return title - - -def wait_for_frames(procs: list[Popen]): - sm = SubMaster(['uiDebug']) - no_frames_drawn = True - while no_frames_drawn: - sm.update() - no_frames_drawn = sm['uiDebug'].drawTimeMillis == 0. - check_for_failure(procs) - - -def clip( - data_dir: str | None, - quality: Literal['low', 'high'], - prefix: str, - route: Route, - out: str, - start: int, - end: int, - speed: int, - target_mb: int, - title: str | None, -): - logger.info(f'clipping route {route.name.canonical_name}, start={start} end={end} quality={quality} target_filesize={target_mb}MB') - lr = get_logreader(route) - - begin_at = max(start - SECONDS_TO_WARM, 0) - duration = end - start - bit_rate_kbps = int(round(target_mb * 8 * 1024 * 1024 / duration / 1000)) - - # TODO: evaluate creating fn that inspects /tmp/.X11-unix and creates unused display to avoid possibility of collision - display = f':{randint(99, 999)}' - - box_style = 'box=1:boxcolor=black@0.33:boxborderw=7' - meta_text = get_meta_text(lr, route) - overlays = [ - # metadata overlay - f"drawtext=text='{escape_ffmpeg_text(meta_text)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=15:{box_style}:x=(w-text_w)/2:y=5.5:enable='between(t,1,5)'", - # route time overlay - f"drawtext=text='%{{eif\\:floor(({start}+t)/60)\\:d\\:2}}\\:%{{eif\\:mod({start}+t\\,60)\\:d\\:2}}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=24:{box_style}:x=w-text_w-38:y=38" - ] if title: - overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=32:{box_style}:x=(w-text_w)/2:y=53") + draw_text_box(rl, title, 0, 60, 32, gui_app, font, font_scale, center=True) - if speed > 1: - overlays += [ - f"setpts=PTS/{speed}", - "fps=60", - ] + if show_time: + t = start_time + frame_idx / FRAMERATE + time_text = f"{int(t)//60:02d}:{int(t)%60:02d}" + time_width = int(rl.measure_text_ex(font, time_text, 24 * font_scale, 0).x) + draw_text_box(rl, time_text, gui_app.width - time_width - 45, 45, 24, gui_app, font, font_scale) - ffmpeg_cmd = [ - 'ffmpeg', '-y', - '-video_size', RESOLUTION, - '-framerate', str(FRAMERATE), - '-f', 'x11grab', - '-rtbufsize', '100M', - '-draw_mouse', '0', - '-i', display, - '-c:v', 'libx264', - '-maxrate', f'{bit_rate_kbps}k', - '-bufsize', f'{bit_rate_kbps*2}k', - '-crf', '23', - '-filter:v', ','.join(overlays), - '-preset', 'ultrafast', - '-tune', 'zerolatency', - '-pix_fmt', 'yuv420p', - '-movflags', '+faststart', - '-f', 'mp4', - '-t', str(duration), - out, - ] - replay_cmd = [REPLAY, '--ecam', '-c', '1', '-s', str(begin_at), '--prefix', prefix] - if data_dir: - replay_cmd.extend(['--data_dir', data_dir]) - if quality == 'low': - replay_cmd.append('--qcam') - replay_cmd.append(route.name.canonical_name) +def clip(route: Route, output: str, start: int, end: int, headless: bool = True, big: bool = False, + title: str | None = None, show_metadata: bool = True, show_time: bool = True, use_qcam: bool = False): + timer, duration = Timer(), end - start - ui_cmd = [UI, '-platform', 'xcb'] - xvfb_cmd = ['Xvfb', display, '-terminate', '-screen', '0', f'{RESOLUTION}x{PIXEL_DEPTH}'] + import pyray as rl + if big: + from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView + else: + from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView # type: ignore[assignment] + from openpilot.selfdrive.ui.ui_state import ui_state + from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE + timer.lap("import") - with OpenpilotPrefix(prefix, shared_download_cache=True): - populate_car_params(lr) - env = os.environ.copy() - env['DISPLAY'] = display + logger.info(f"Clipping {route.name.canonical_name}, {start}s-{end}s ({duration}s)") + seg_start, seg_end = start // 60, (end - 1) // 60 + 1 + all_chunks = load_logs_parallel(route.log_paths()[seg_start:seg_end], fps=FRAMERATE) + timer.lap("logs") - with managed_proc(xvfb_cmd, env) as xvfb_proc, managed_proc(ui_cmd, env) as ui_proc, managed_proc(replay_cmd, env) as replay_proc: - procs = [xvfb_proc, ui_proc, replay_proc] - logger.info('waiting for replay to begin (loading segments, may take a while)...') - wait_for_frames(procs) - logger.debug(f'letting UI warm up ({SECONDS_TO_WARM}s)...') - time.sleep(SECONDS_TO_WARM) - check_for_failure(procs) - with managed_proc(ffmpeg_cmd, env) as ffmpeg_proc: - procs.append(ffmpeg_proc) - logger.info(f'recording in progress ({duration}s)...') - ffmpeg_proc.wait(duration + PROC_WAIT_SECONDS) - check_for_failure(procs) - logger.info(f'recording complete: {Path(out).resolve()}') + frame_start = (start - seg_start * 60) * FRAMERATE + message_chunks = all_chunks[frame_start:frame_start + duration * FRAMERATE] + if not message_chunks: + logger.error("No messages to render") + sys.exit(1) + + metadata = load_route_metadata(route) if show_metadata else None + if headless: + rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) + + with OpenpilotPrefix(shared_download_cache=True): + camera_paths = route.qcamera_paths() if use_qcam else route.camera_paths() + frame_queue = FrameQueue(camera_paths, start, end, fps=FRAMERATE, use_qcam=use_qcam) + + vipc = VisionIpcServer("camerad") + vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 4, frame_queue.frame_w, frame_queue.frame_h) + vipc.start_listener() + + patch_submaster(message_chunks, ui_state) + gui_app.init_window("clip", fps=FRAMERATE) + + road_view = AugmentedRoadView() + road_view.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + font = gui_app.font(FontWeight.NORMAL) + timer.lap("setup") + + frame_idx = 0 + with tqdm.tqdm(total=len(message_chunks), desc="Rendering", unit="frame") as pbar: + for should_render in gui_app.render(): + if frame_idx >= len(message_chunks): + break + _, frame_bytes = frame_queue.get() + vipc.send(VisionStreamType.VISION_STREAM_ROAD, frame_bytes, frame_idx, int(frame_idx * 5e7), int(frame_idx * 5e7)) + ui_state.update() + if should_render: + road_view.render() + render_overlays(rl, gui_app, font, FONT_SCALE, metadata, title, start, frame_idx, show_metadata, show_time) + frame_idx += 1 + pbar.update(1) + timer.lap("render") + + frame_queue.stop() + gui_app.close() + timer.lap("ffmpeg") + + logger.info(f"Clip saved to: {Path(output).resolve()}") + logger.info(f"Generated {timer.fmt(duration)}") def main(): - p = ArgumentParser(prog='clip.py', description='clip your openpilot route.', epilog='comma.ai') - validate_env(p) - route_group = p.add_mutually_exclusive_group(required=True) - route_group.add_argument('route', nargs='?', type=validate_route, help=f'The route (e.g. {DEMO_ROUTE} or {DEMO_ROUTE}/{DEMO_START}/{DEMO_END})') - route_group.add_argument('--demo', help='use the demo route', action='store_true') - p.add_argument('-d', '--data-dir', help='local directory where route data is stored') - p.add_argument('-e', '--end', help='stop clipping at seconds', type=int) - p.add_argument('-f', '--file-size', help='target file size (Discord/GitHub support max 10MB, default is 9MB)', type=float, default=9.) - p.add_argument('-o', '--output', help='output clip to (.mp4)', type=validate_output_file, default=DEFAULT_OUTPUT) - p.add_argument('-p', '--prefix', help='openpilot prefix', default=f'clip_{randint(100, 99999)}') - p.add_argument('-q', '--quality', help='quality of camera (low = qcam, high = hevc)', choices=['low', 'high'], default='high') - p.add_argument('-x', '--speed', help='record the clip at this speed multiple', type=int, default=1) - p.add_argument('-s', '--start', help='start clipping at seconds', type=int) - p.add_argument('-t', '--title', help='overlay this title on the video (e.g. "Chill driving across the Golden Gate Bridge")', type=validate_title) - args = parse_args(p) - exit_code = 1 - try: - clip( - data_dir=args.data_dir, - quality=args.quality, - prefix=args.prefix, - route=args.route, - out=args.output, - start=args.start, - end=args.end, - speed=args.speed, - target_mb=args.file_size, - title=args.title, - ) - exit_code = 0 - except KeyboardInterrupt as e: - logger.exception('interrupted by user', exc_info=e) - except Exception as e: - logger.exception('encountered error', exc_info=e) - sys.exit(exit_code) + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s\t%(message)s") + args = parse_args() + assert args.big, "Clips doesn't support mici UI yet. TODO: make it work" + + setup_env(args.output, big=args.big, speed=args.speed, target_mb=args.file_size, duration=args.end - args.start) + clip(Route(args.route, data_dir=args.data_dir), args.output, args.start, args.end, not args.windowed, + args.big, args.title, not args.no_metadata, not args.no_time_overlay, args.qcam) -if __name__ == '__main__': - logging.basicConfig(level=logging.INFO, format='%(asctime)s %(name)s %(levelname)s\t%(message)s') +if __name__ == "__main__": main() diff --git a/tools/lib/tests/test_caching.py b/tools/lib/tests/test_caching.py index 6e70ef90b0..cb14098e6d 100644 --- a/tools/lib/tests/test_caching.py +++ b/tools/lib/tests/test_caching.py @@ -56,13 +56,13 @@ class TestFileDownload: for k, v in retry_defaults.items(): 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) + # ensure caching on by default and cache dir gets created + os.environ.pop("DISABLE_FILEREADER_CACHE", None) if os.path.exists(Paths.download_cache_root()): shutil.rmtree(Paths.download_cache_root()) URLFile(f"{host}/test.txt").get_length() URLFile(f"{host}/test.txt").read() - assert not os.path.exists(Paths.download_cache_root()) + assert os.path.exists(Paths.download_cache_root()) def compare_loads(self, url, start=0, length=None): """Compares range between cached and non cached version""" @@ -90,7 +90,7 @@ class TestFileDownload: def test_small_file(self): # Make sure we don't force cache - os.environ["FILEREADER_CACHE"] = "0" + os.environ.pop("DISABLE_FILEREADER_CACHE", None) small_file_url = "https://raw.githubusercontent.com/commaai/openpilot/master/docs/SAFETY.md" # If you want large file to be larger than a chunk # large_file_url = "https://commadataci.blob.core.windows.net/openpilotci/0375fdf7b1ce594d/2019-06-13--08-32-25/3/fcamera.hevc" @@ -119,7 +119,10 @@ class TestFileDownload: @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" + if cache_enabled: + os.environ.pop("DISABLE_FILEREADER_CACHE", None) + else: + os.environ["DISABLE_FILEREADER_CACHE"] = "1" file_url = f"{host}/test.png" diff --git a/tools/lib/tests/test_logreader.py b/tools/lib/tests/test_logreader.py index ee75a8b1ce..0151940c44 100644 --- a/tools/lib/tests/test_logreader.py +++ b/tools/lib/tests/test_logreader.py @@ -93,7 +93,10 @@ class TestLogReader: @pytest.mark.parametrize("cache_enabled", [True, False]) def test_direct_parsing(self, mocker, cache_enabled): file_exists_mock = mocker.patch("openpilot.tools.lib.filereader.file_exists") - os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0" + if cache_enabled: + os.environ.pop("DISABLE_FILEREADER_CACHE", None) + else: + os.environ["DISABLE_FILEREADER_CACHE"] = "1" qlog = tempfile.NamedTemporaryFile(mode='wb', delete=False) with requests.get(QLOG_FILE, stream=True) as r: @@ -181,7 +184,10 @@ class TestLogReader: @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" + if cache_enabled: + os.environ.pop("DISABLE_FILEREADER_CACHE", None) + else: + os.environ["DISABLE_FILEREADER_CACHE"] = "1" lr = LogReader(f"{TEST_ROUTE}/0:4") assert len(lr.run_across_segments(4, noop)) == len(list(lr)) diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index 790fa7e8fb..de12070465 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -74,8 +74,8 @@ class URLFile: self._timeout = Timeout(connect=timeout, read=timeout) self._pos = 0 self._length: int | None = None - # True by default, false if FILEREADER_CACHE is defined, but can be overwritten by the cache input - self._force_download = not int(os.environ.get("FILEREADER_CACHE", "0")) + # Caching enabled by default, can be disabled with DISABLE_FILEREADER_CACHE=1, or overwritten by the cache input + self._force_download = int(os.environ.get("DISABLE_FILEREADER_CACHE", "0")) == 1 if cache is not None: self._force_download = not cache @@ -192,8 +192,25 @@ class URLFile: raise URLFileException(f"Expected {len(ranges)} parts, got {len(parts)} ({self._url})") return parts - def seek(self, pos: int) -> None: - self._pos = int(pos) + def seekable(self) -> bool: + return True + + def seek(self, pos: int, whence: int = 0) -> int: + pos = int(pos) + if whence == os.SEEK_SET: + self._pos = pos + elif whence == os.SEEK_CUR: + self._pos += pos + elif whence == os.SEEK_END: + length = self.get_length() + assert length != -1, "Cannot seek from end on unknown length file" + self._pos = length + pos + else: + raise URLFileException("Invalid whence value") + return self._pos + + def tell(self) -> int: + return self._pos @property def name(self) -> str: diff --git a/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py b/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py new file mode 100644 index 0000000000..8c1a60f5b7 --- /dev/null +++ b/tools/longitudinal_maneuvers/mpc_longitudinal_tuning_report.py @@ -0,0 +1,279 @@ +import io +import sys +import markdown +import numpy as np +import matplotlib.pyplot as plt +from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver +from openpilot.selfdrive.controls.tests.test_following_distance import desired_follow_distance + +TIME = 0 +LEAD_DISTANCE= 2 +EGO_V = 3 +EGO_A = 5 +D_REL = 6 + +axis_labels = ['Time (s)', + 'Ego position (m)', + 'Lead absolute position (m)', + 'Ego Velocity (m/s)', + 'Lead Velocity (m/s)', + 'Ego acceleration (m/s^2)', + 'Lead distance (m)' + ] + + +def get_html_from_results(results, labels, AXIS): + fig, ax = plt.subplots(figsize=(16, 8)) + for idx, speed in enumerate(list(results.keys())): + ax.plot(results[speed][:, TIME], results[speed][:, AXIS], label=labels[idx]) + + ax.set_xlabel('Time (s)') + ax.set_ylabel(axis_labels[AXIS]) + ax.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0) + ax.grid(True, linestyle='--', alpha=0.7) + ax.text(-0.075, 0.5, '.', transform=ax.transAxes, color='none') + + fig_buffer = io.StringIO() + fig.savefig(fig_buffer, format='svg', bbox_inches='tight') + plt.close(fig) + return fig_buffer.getvalue() + '
' + + +htmls = [] + +results = {} +name = 'Resuming behind lead' +labels = [] +for lead_accel in np.linspace(1.0, 4.0, 4): + man = Maneuver( + '', + duration=11, + initial_speed=0.0, + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(0.0, 0.0), + speed_lead_values=[0.0, 10 * lead_accel], + cruise_values=[100, 100], + prob_lead_values=[1.0, 1.0], + breakpoints=[1., 11], + ) + valid, results[lead_accel] = man.evaluate() + labels.append(f'{lead_accel} m/s^2 lead acceleration') + +htmls.append(markdown.markdown('# ' + name)) +htmls.append(get_html_from_results(results, labels, EGO_V)) +htmls.append(get_html_from_results(results, labels, EGO_A)) + + +results = {} +name = 'Approaching stopped car from 140m' +labels = [] +for speed in np.arange(0,45,5): + man = Maneuver( + name, + duration=30., + initial_speed=float(speed), + lead_relevancy=True, + initial_distance_lead=140., + speed_lead_values=[0.0, 0.], + breakpoints=[0., 30.], + ) + valid, results[speed] = man.evaluate() + results[speed][:,2] = results[speed][:,2] - results[speed][:,1] + labels.append(f'{speed} m/s approach speed') + +htmls.append(markdown.markdown('# ' + name)) +htmls.append(get_html_from_results(results, labels, EGO_A)) +htmls.append(get_html_from_results(results, labels, D_REL)) + + +results = {} +name = 'Following 5s oscillating lead' +labels = [] +speed = np.int64(10) +for oscil in np.arange(0, 10, 1): + man = Maneuver( + '', + duration=30., + initial_speed=float(speed), + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(speed, speed), + speed_lead_values=[speed, speed, speed - oscil, speed + oscil, speed - oscil, speed + oscil, speed - oscil], + breakpoints=[0.,2., 5, 8, 15, 18, 25.], + ) + valid, results[oscil] = man.evaluate() + labels.append(f'{oscil} m/s oscilliation size') + +htmls.append(markdown.markdown('# ' + name)) +htmls.append(get_html_from_results(results, labels, D_REL)) +htmls.append(get_html_from_results(results, labels, EGO_V)) +htmls.append(get_html_from_results(results, labels, EGO_A)) + + + +results = {} +name = 'Speed profile when converging to steady state lead at 30m/s' +labels = [] +for distance in np.arange(20, 140, 10): + man = Maneuver( + '', + duration=50, + initial_speed=30.0, + lead_relevancy=True, + initial_distance_lead=distance, + speed_lead_values=[30.0], + breakpoints=[0.], + ) + valid, results[distance] = man.evaluate() + results[distance][:,2] = results[distance][:,2] - results[distance][:,1] + labels.append(f'{distance} m initial distance') + +htmls.append(markdown.markdown('# ' + name)) +htmls.append(get_html_from_results(results, labels, EGO_V)) +htmls.append(get_html_from_results(results, labels, D_REL)) + + +results = {} +name = 'Speed profile when converging to steady state lead at 20m/s' +labels = [] +for distance in np.arange(20, 140, 10): + man = Maneuver( + '', + duration=50, + initial_speed=20.0, + lead_relevancy=True, + initial_distance_lead=distance, + speed_lead_values=[20.0], + breakpoints=[0.], + ) + valid, results[distance] = man.evaluate() + results[distance][:,2] = results[distance][:,2] - results[distance][:,1] + labels.append(f'{distance} m initial distance') + +htmls.append(markdown.markdown('# ' + name)) +htmls.append(get_html_from_results(results, labels, EGO_V)) +htmls.append(get_html_from_results(results, labels, D_REL)) + + +results = {} +name = 'Following car at 30m/s that comes to a stop' +labels = [] +for stop_time in np.arange(4, 14, 1): + man = Maneuver( + '', + duration=50, + initial_speed=30.0, + lead_relevancy=True, + initial_distance_lead=60.0, + speed_lead_values=[30.0, 30.0, 0.0, 0.0], + breakpoints=[0., 20., 20 + stop_time, 30 + stop_time], + ) + valid, results[stop_time] = man.evaluate() + results[stop_time][:,2] = results[stop_time][:,2] - results[stop_time][:,1] + labels.append(f'{stop_time} seconds stop time') + +htmls.append(markdown.markdown('# ' + name)) +htmls.append(get_html_from_results(results, labels, EGO_A)) +htmls.append(get_html_from_results(results, labels, D_REL)) + + +results = {} +name = 'Response to cut-in at half follow distance' +labels = [] +for speed in np.arange(0, 40, 5): + man = Maneuver( + '', + duration=10, + initial_speed=float(speed), + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(speed, speed)/2, + speed_lead_values=[speed, speed, speed], + cruise_values=[speed, speed, speed], + prob_lead_values=[0.0, 0.0, 1.0], + breakpoints=[0., 5.0, 5.01], + ) + valid, results[speed] = man.evaluate() + labels.append(f'{speed} m/s speed') + +htmls.append(markdown.markdown('# ' + name)) +htmls.append(get_html_from_results(results, labels, EGO_A)) +htmls.append(get_html_from_results(results, labels, D_REL)) + + +results = {} +name = 'Follow a lead that accelerates at 2m/s^2 until steady state speed' +labels = [] +for speed in np.arange(0, 40, 5): + man = Maneuver( + '', + duration=50, + initial_speed=0.0, + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(0.0, 0.0), + speed_lead_values=[0.0, 0.0, speed], + prob_lead_values=[1.0, 1.0, 1.0], + breakpoints=[0., 1.0, speed/2], + ) + valid, results[speed] = man.evaluate() + labels.append(f'{speed} m/s speed') + +htmls.append(markdown.markdown('# ' + name)) +htmls.append(get_html_from_results(results, labels, EGO_V)) +htmls.append(get_html_from_results(results, labels, EGO_A)) + + +results = {} +name = 'From stop to cruise' +labels = [] +for speed in np.arange(0, 40, 5): + man = Maneuver( + '', + duration=50, + initial_speed=0.0, + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(0.0, 0.0), + speed_lead_values=[0.0, 0.0], + cruise_values=[0.0, speed], + prob_lead_values=[0.0, 0.0], + breakpoints=[1., 1.01], + ) + valid, results[speed] = man.evaluate() + labels.append(f'{speed} m/s speed') + +htmls.append(markdown.markdown('# ' + name)) +htmls.append(get_html_from_results(results, labels, EGO_V)) +htmls.append(get_html_from_results(results, labels, EGO_A)) + + +results = {} +name = 'From cruise to min' +labels = [] +for speed in np.arange(10, 40, 5): + man = Maneuver( + '', + duration=50, + initial_speed=float(speed), + lead_relevancy=True, + initial_distance_lead=desired_follow_distance(0.0, 0.0), + speed_lead_values=[0.0, 0.0], + cruise_values=[speed, 10.0], + prob_lead_values=[0.0, 0.0], + breakpoints=[1., 1.01], + ) + valid, results[speed] = man.evaluate() + labels.append(f'{speed} m/s speed') + +htmls.append(markdown.markdown('# ' + name)) +htmls.append(get_html_from_results(results, labels, EGO_V)) +htmls.append(get_html_from_results(results, labels, EGO_A)) + +if len(sys.argv) < 2: + file_name = 'long_mpc_tune_report.html' +else: + file_name = sys.argv[1] + +with open(file_name, 'w') as f: + f.write(markdown.markdown('# MPC longitudinal tuning report')) + +with open(file_name, 'a') as f: + for html in htmls: + f.write(html) diff --git a/tools/plotjuggler/juggle.py b/tools/plotjuggler/juggle.py index 34f33d1959..142e640504 100755 --- a/tools/plotjuggler/juggle.py +++ b/tools/plotjuggler/juggle.py @@ -47,7 +47,7 @@ def install(): tmpf.write(chunk) with tarfile.open(tmp.name) as tar: - tar.extractall(path=INSTALL_DIR) + tar.extractall(path=INSTALL_DIR, filter="data") def get_plotjuggler_version(): diff --git a/tools/replay/can_replay.py b/tools/replay/can_replay.py index 13c30a62ad..0717320077 100755 --- a/tools/replay/can_replay.py +++ b/tools/replay/can_replay.py @@ -5,8 +5,6 @@ import time import usb1 import threading -os.environ['FILEREADER_CACHE'] = '1' - from openpilot.common.realtime import config_realtime_process, Ratekeeper, DT_CTRL from openpilot.selfdrive.pandad import can_capnp_to_list from openpilot.tools.lib.logreader import LogReader diff --git a/tools/sim/lib/camerad.py b/tools/sim/lib/camerad.py index be4e1a610c..7634b8524d 100644 --- a/tools/sim/lib/camerad.py +++ b/tools/sim/lib/camerad.py @@ -1,14 +1,39 @@ import numpy as np -import os -import pyopencl as cl -import pyopencl.array as cl_array from msgq.visionipc import VisionIpcServer, VisionStreamType from cereal import messaging -from openpilot.common.basedir import BASEDIR from openpilot.tools.sim.lib.common import W, H + +def rgb_to_nv12(rgb): + """Convert RGB image to NV12 (YUV420) format using BT.601 coefficients.""" + h, w = rgb.shape[:2] + r = rgb[:, :, 0].astype(np.int32) + g = rgb[:, :, 1].astype(np.int32) + b = rgb[:, :, 2].astype(np.int32) + + # Y plane - BT.601 coefficients (matches original OpenCL kernel) + y = (((b * 13 + g * 65 + r * 33) + 64) >> 7) + 16 + y = np.clip(y, 0, 255).astype(np.uint8) + + # Subsample RGB for UV (2x2 box filter) + r_sub = (r[0::2, 0::2] + r[0::2, 1::2] + r[1::2, 0::2] + r[1::2, 1::2] + 2) >> 2 + g_sub = (g[0::2, 0::2] + g[0::2, 1::2] + g[1::2, 0::2] + g[1::2, 1::2] + 2) >> 2 + b_sub = (b[0::2, 0::2] + b[0::2, 1::2] + b[1::2, 0::2] + b[1::2, 1::2] + 2) >> 2 + + # U and V planes + u = np.clip((b_sub * 56 - g_sub * 37 - r_sub * 19 + 0x8080) >> 8, 0, 255).astype(np.uint8) + v = np.clip((r_sub * 56 - g_sub * 47 - b_sub * 9 + 0x8080) >> 8, 0, 255).astype(np.uint8) + + # Interleave UV for NV12 format + uv = np.empty((h // 2, w), dtype=np.uint8) + uv[:, 0::2] = u + uv[:, 1::2] = v + + return np.concatenate([y.ravel(), uv.ravel()]).tobytes() + + class Camerad: """Simulates the camerad daemon""" def __init__(self, dual_camera): @@ -24,18 +49,6 @@ class Camerad: self.vipc_server.start_listener() - # set up for pyopencl rgb to yuv conversion - self.ctx = cl.create_some_context() - self.queue = cl.CommandQueue(self.ctx) - cl_arg = f" -DHEIGHT={H} -DWIDTH={W} -DRGB_STRIDE={W * 3} -DUV_WIDTH={W // 2} -DUV_HEIGHT={H // 2} -DRGB_SIZE={W * H} -DCL_DEBUG " - - kernel_fn = os.path.join(BASEDIR, "tools/sim/rgb_to_nv12.cl") - with open(kernel_fn) as f: - prg = cl.Program(self.ctx, f.read()).build(cl_arg) - self.krnl = prg.rgb_to_nv12 - self.Wdiv4 = W // 4 if (W % 4 == 0) else (W + (4 - W % 4)) // 4 - self.Hdiv4 = H // 4 if (H % 4 == 0) else (H + (4 - H % 4)) // 4 - def cam_send_yuv_road(self, yuv): self._send_yuv(yuv, self.frame_road_id, 'roadCameraState', VisionStreamType.VISION_STREAM_ROAD) self.frame_road_id += 1 @@ -44,16 +57,11 @@ class Camerad: self._send_yuv(yuv, self.frame_wide_id, 'wideRoadCameraState', VisionStreamType.VISION_STREAM_WIDE_ROAD) self.frame_wide_id += 1 - # Returns: yuv bytes def rgb_to_yuv(self, rgb): + """Convert RGB to NV12 YUV format.""" assert rgb.shape == (H, W, 3), f"{rgb.shape}" assert rgb.dtype == np.uint8 - - rgb_cl = cl_array.to_device(self.queue, rgb) - yuv_cl = cl_array.empty_like(rgb_cl) - self.krnl(self.queue, (self.Wdiv4, self.Hdiv4), None, rgb_cl.data, yuv_cl.data).wait() - yuv = np.resize(yuv_cl.get(), rgb.size // 2) - return yuv.data.tobytes() + return rgb_to_nv12(rgb) def _send_yuv(self, yuv, frame_id, pub_type, yuv_type): eof = int(frame_id * 0.05 * 1e9) diff --git a/tools/sim/rgb_to_nv12.cl b/tools/sim/rgb_to_nv12.cl deleted file mode 100644 index 54816d5d7d..0000000000 --- a/tools/sim/rgb_to_nv12.cl +++ /dev/null @@ -1,119 +0,0 @@ -#define RGB_TO_Y(r, g, b) ((((mul24(b, 13) + mul24(g, 65) + mul24(r, 33)) + 64) >> 7) + 16) -#define RGB_TO_U(r, g, b) ((mul24(b, 56) - mul24(g, 37) - mul24(r, 19) + 0x8080) >> 8) -#define RGB_TO_V(r, g, b) ((mul24(r, 56) - mul24(g, 47) - mul24(b, 9) + 0x8080) >> 8) -#define AVERAGE(x, y, z, w) ((convert_ushort(x) + convert_ushort(y) + convert_ushort(z) + convert_ushort(w) + 1) >> 1) - -inline void convert_2_ys(__global uchar * out_yuv, int yi, const uchar8 rgbs1) { - uchar2 yy = (uchar2)( - RGB_TO_Y(rgbs1.s2, rgbs1.s1, rgbs1.s0), - RGB_TO_Y(rgbs1.s5, rgbs1.s4, rgbs1.s3) - ); -#ifdef CL_DEBUG - if(yi >= RGB_SIZE) - printf("Y vector2 overflow, %d > %d\n", yi, RGB_SIZE); -#endif - vstore2(yy, 0, out_yuv + yi); -} - -inline void convert_4_ys(__global uchar * out_yuv, int yi, const uchar8 rgbs1, const uchar8 rgbs3) { - const uchar4 yy = (uchar4)( - RGB_TO_Y(rgbs1.s2, rgbs1.s1, rgbs1.s0), - RGB_TO_Y(rgbs1.s5, rgbs1.s4, rgbs1.s3), - RGB_TO_Y(rgbs3.s0, rgbs1.s7, rgbs1.s6), - RGB_TO_Y(rgbs3.s3, rgbs3.s2, rgbs3.s1) - ); -#ifdef CL_DEBUG - if(yi > RGB_SIZE - 4) - printf("Y vector4 overflow, %d > %d\n", yi, RGB_SIZE - 4); -#endif - vstore4(yy, 0, out_yuv + yi); -} - -inline void convert_uv(__global uchar * out_yuv, int uvi, - const uchar8 rgbs1, const uchar8 rgbs2) { - // U & V: average of 2x2 pixels square - const short ab = AVERAGE(rgbs1.s0, rgbs1.s3, rgbs2.s0, rgbs2.s3); - const short ag = AVERAGE(rgbs1.s1, rgbs1.s4, rgbs2.s1, rgbs2.s4); - const short ar = AVERAGE(rgbs1.s2, rgbs1.s5, rgbs2.s2, rgbs2.s5); -#ifdef CL_DEBUG - if(uvi >= RGB_SIZE + RGB_SIZE / 2) - printf("UV overflow, %d >= %d\n", uvi, RGB_SIZE + RGB_SIZE / 2); -#endif - out_yuv[uvi] = RGB_TO_U(ar, ag, ab); - out_yuv[uvi+1] = RGB_TO_V(ar, ag, ab); -} - -inline void convert_2_uvs(__global uchar * out_yuv, int uvi, - const uchar8 rgbs1, const uchar8 rgbs2, const uchar8 rgbs3, const uchar8 rgbs4) { - // U & V: average of 2x2 pixels square - const short ab1 = AVERAGE(rgbs1.s0, rgbs1.s3, rgbs2.s0, rgbs2.s3); - const short ag1 = AVERAGE(rgbs1.s1, rgbs1.s4, rgbs2.s1, rgbs2.s4); - const short ar1 = AVERAGE(rgbs1.s2, rgbs1.s5, rgbs2.s2, rgbs2.s5); - const short ab2 = AVERAGE(rgbs1.s6, rgbs3.s1, rgbs2.s6, rgbs4.s1); - const short ag2 = AVERAGE(rgbs1.s7, rgbs3.s2, rgbs2.s7, rgbs4.s2); - const short ar2 = AVERAGE(rgbs3.s0, rgbs3.s3, rgbs4.s0, rgbs4.s3); - uchar4 uv = (uchar4)( - RGB_TO_U(ar1, ag1, ab1), - RGB_TO_V(ar1, ag1, ab1), - RGB_TO_U(ar2, ag2, ab2), - RGB_TO_V(ar2, ag2, ab2) - ); -#ifdef CL_DEBUG1 - if(uvi > RGB_SIZE + RGB_SIZE / 2 - 4) - printf("UV2 overflow, %d >= %d\n", uvi, RGB_SIZE + RGB_SIZE / 2 - 2); -#endif - vstore4(uv, 0, out_yuv + uvi); -} - -__kernel void rgb_to_nv12(__global uchar const * const rgb, - __global uchar * out_yuv) -{ - const int dx = get_global_id(0); - const int dy = get_global_id(1); - const int col = mul24(dx, 4); // Current column in rgb image - const int row = mul24(dy, 4); // Current row in rgb image - const int bgri_start = mad24(row, RGB_STRIDE, mul24(col, 3)); // Start offset of rgb data being converted - const int yi_start = mad24(row, WIDTH, col); // Start offset in the target yuv buffer - int uvi = mad24(row / 2, WIDTH, RGB_SIZE + col); - int num_col = min(WIDTH - col, 4); - int num_row = min(HEIGHT - row, 4); - if(num_row == 4) { - const uchar8 rgbs0_0 = vload8(0, rgb + bgri_start); - const uchar8 rgbs0_1 = vload8(0, rgb + bgri_start + 8); - const uchar8 rgbs1_0 = vload8(0, rgb + bgri_start + RGB_STRIDE); - const uchar8 rgbs1_1 = vload8(0, rgb + bgri_start + RGB_STRIDE + 8); - const uchar8 rgbs2_0 = vload8(0, rgb + bgri_start + RGB_STRIDE * 2); - const uchar8 rgbs2_1 = vload8(0, rgb + bgri_start + RGB_STRIDE * 2 + 8); - const uchar8 rgbs3_0 = vload8(0, rgb + bgri_start + RGB_STRIDE * 3); - const uchar8 rgbs3_1 = vload8(0, rgb + bgri_start + RGB_STRIDE * 3 + 8); - if(num_col == 4) { - convert_4_ys(out_yuv, yi_start, rgbs0_0, rgbs0_1); - convert_4_ys(out_yuv, yi_start + WIDTH, rgbs1_0, rgbs1_1); - convert_4_ys(out_yuv, yi_start + WIDTH * 2, rgbs2_0, rgbs2_1); - convert_4_ys(out_yuv, yi_start + WIDTH * 3, rgbs3_0, rgbs3_1); - convert_2_uvs(out_yuv, uvi, rgbs0_0, rgbs1_0, rgbs0_1, rgbs1_1); - convert_2_uvs(out_yuv, uvi + WIDTH, rgbs2_0, rgbs3_0, rgbs2_1, rgbs3_1); - } else if(num_col == 2) { - convert_2_ys(out_yuv, yi_start, rgbs0_0); - convert_2_ys(out_yuv, yi_start + WIDTH, rgbs1_0); - convert_2_ys(out_yuv, yi_start + WIDTH * 2, rgbs2_0); - convert_2_ys(out_yuv, yi_start + WIDTH * 3, rgbs3_0); - convert_uv(out_yuv, uvi, rgbs0_0, rgbs1_0); - convert_uv(out_yuv, uvi + WIDTH, rgbs2_0, rgbs3_0); - } - } else { - const uchar8 rgbs0_0 = vload8(0, rgb + bgri_start); - const uchar8 rgbs0_1 = vload8(0, rgb + bgri_start + 8); - const uchar8 rgbs1_0 = vload8(0, rgb + bgri_start + RGB_STRIDE); - const uchar8 rgbs1_1 = vload8(0, rgb + bgri_start + RGB_STRIDE + 8); - if(num_col == 4) { - convert_4_ys(out_yuv, yi_start, rgbs0_0, rgbs0_1); - convert_4_ys(out_yuv, yi_start + WIDTH, rgbs1_0, rgbs1_1); - convert_2_uvs(out_yuv, uvi, rgbs0_0, rgbs1_0, rgbs0_1, rgbs1_1); - } else if(num_col == 2) { - convert_2_ys(out_yuv, yi_start, rgbs0_0); - convert_2_ys(out_yuv, yi_start + WIDTH, rgbs1_0); - convert_uv(out_yuv, uvi, rgbs0_0, rgbs1_0); - } - } -} diff --git a/tools/sim/tests/test_metadrive_bridge.py b/tools/sim/tests/test_metadrive_bridge.py index 04ce5d584f..9be640d736 100644 --- a/tools/sim/tests/test_metadrive_bridge.py +++ b/tools/sim/tests/test_metadrive_bridge.py @@ -8,7 +8,6 @@ from openpilot.tools.sim.bridge.metadrive.metadrive_bridge import MetaDriveBridg 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): diff --git a/tools/tuning/measure_steering_accuracy.py b/tools/tuning/measure_steering_accuracy.py index e4aef0ba15..ae3344c2eb 100755 --- a/tools/tuning/measure_steering_accuracy.py +++ b/tools/tuning/measure_steering_accuracy.py @@ -117,12 +117,8 @@ if __name__ == "__main__": parser.add_argument('--route', help="route name") parser.add_argument('--addr', default='127.0.0.1', help="IP address for optional ZMQ listener, default to msgq") parser.add_argument('--group', default='all', help="speed group to display, [crawl|slow|medium|fast|veryfast|germany|all], default to all") - parser.add_argument('--cache', default=False, action='store_true', help="use cached data, default to False") args = parser.parse_args() - if args.cache: - os.environ['FILEREADER_CACHE'] = '1' - tool = SteeringAccuracyTool(args) if args.route is not None: diff --git a/uv.lock b/uv.lock index 674b18b7ea..054a67cc9a 100644 --- a/uv.lock +++ b/uv.lock @@ -378,37 +378,37 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.1" +version = "7.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, - { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, - { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, - { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] [[package]] @@ -633,15 +633,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] -[[package]] -name = "future-fstrings" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5d/e2/3874574cce18a2e3608abfe5b4b5b3c9765653c464f5da18df8971cf501d/future_fstrings-1.2.0.tar.gz", hash = "sha256:6cf41cbe97c398ab5a81168ce0dbb8ad95862d3caf23c21e4430627b90844089", size = 5786, upload-time = "2019-06-16T03:04:42.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/6d/ea1d52e9038558dd37f5d30647eb9f07888c164960a5d4daa5f970c6da25/future_fstrings-1.2.0-py2.py3-none-any.whl", hash = "sha256:90e49598b553d8746c4dc7d9442e0359d038c3039d802c91c0a55505da318c63", size = 6138, upload-time = "2019-06-16T03:04:40.395Z" }, -] - [[package]] name = "ghp-import" version = "2.1.0" @@ -777,15 +768,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/9e/820c4b086ad01ba7d77369fb8b11470a01fac9b4977f02e18659cf378b6b/json_rpc-1.15.0-py2.py3-none-any.whl", hash = "sha256:4a4668bbbe7116feb4abbd0f54e64a4adcf4b8f648f19ffa0848ad0f6606a9bf", size = 39450, upload-time = "2023-06-11T09:45:47.136Z" }, ] -[[package]] -name = "kaitaistruct" -version = "0.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/b8/ca7319556912f68832daa4b81425314857ec08dfccd8dbc8c0f65c992108/kaitaistruct-0.11.tar.gz", hash = "sha256:053ee764288e78b8e53acf748e9733268acbd579b8d82a427b1805453625d74b", size = 11519, upload-time = "2025-09-08T15:46:25.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/4a/cf14bf3b1f5ffb13c69cf5f0ea78031247790558ee88984a8bdd22fae60d/kaitaistruct-0.11-py2.py3-none-any.whl", hash = "sha256:5c6ce79177b4e193a577ecd359e26516d1d6d000a0bffd6e1010f2a46a62a561", size = 11372, upload-time = "2025-09-08T15:46:23.635Z" }, -] - [[package]] name = "kiwisolver" version = "1.4.9" @@ -915,11 +897,11 @@ wheels = [ [[package]] name = "markdown" -version = "3.10" +version = "3.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, ] [[package]] @@ -1158,56 +1140,47 @@ wheels = [ [[package]] name = "multidict" -version = "6.7.0" +version = "6.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, - { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, - { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, - { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, - { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, - { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, - { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, - { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, - { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, - { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, - { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, - { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, -] - -[[package]] -name = "natsort" -version = "8.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload-time = "2023-06-20T04:17:19.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] [[package]] @@ -1302,10 +1275,9 @@ dependencies = [ { name = "cffi" }, { name = "crcmod-plus" }, { name = "cython" }, - { name = "future-fstrings" }, { name = "inputs" }, + { name = "jeepney" }, { name = "json-rpc" }, - { name = "kaitaistruct" }, { name = "libusb1" }, { name = "mapbox-earcut" }, { name = "numpy" }, @@ -1325,7 +1297,6 @@ dependencies = [ { name = "sentry-sdk" }, { name = "setproctitle" }, { name = "setuptools" }, - { name = "smbus2" }, { name = "sounddevice" }, { name = "spidev", marker = "sys_platform == 'linux'" }, { name = "sympy" }, @@ -1341,23 +1312,16 @@ dev = [ { name = "azure-identity" }, { name = "azure-storage-blob" }, { name = "dictdiffer" }, - { name = "jeepney" }, { name = "matplotlib" }, { name = "opencv-python-headless" }, { name = "parameterized" }, { name = "pyautogui" }, - { name = "pyopencl" }, - { name = "pyprof2calltree" }, - { name = "pytools", marker = "platform_machine != 'aarch64'" }, { name = "pywinctl" }, { name = "tabulate" }, - { name = "types-requests" }, - { name = "types-tabulate" }, ] docs = [ { name = "jinja2" }, { name = "mkdocs" }, - { name = "natsort" }, ] testing = [ { name = "codespell" }, @@ -1368,7 +1332,6 @@ testing = [ { name = "pytest-asyncio" }, { name = "pytest-cpp" }, { name = "pytest-mock" }, - { name = "pytest-randomly" }, { name = "pytest-repeat" }, { name = "pytest-subtests" }, { name = "pytest-timeout" }, @@ -1396,19 +1359,16 @@ requires-dist = [ { name = "cython" }, { name = "dearpygui", marker = "(platform_machine != 'aarch64' and extra == 'tools') or (sys_platform != 'linux' and extra == 'tools')", specifier = ">=2.1.0" }, { name = "dictdiffer", marker = "extra == 'dev'" }, - { name = "future-fstrings" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, - { name = "jeepney", marker = "extra == 'dev'" }, + { name = "jeepney" }, { name = "jinja2", marker = "extra == 'docs'" }, { name = "json-rpc" }, - { name = "kaitaistruct" }, { name = "libusb1" }, { name = "mapbox-earcut" }, { name = "matplotlib", marker = "extra == 'dev'" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" }, { name = "mkdocs", marker = "extra == 'docs'" }, - { name = "natsort", marker = "extra == 'docs'" }, { name = "numpy", specifier = ">=2.0" }, { name = "onnx", specifier = ">=1.14.0" }, { name = "opencv-python-headless", marker = "extra == 'dev'" }, @@ -1420,20 +1380,16 @@ requires-dist = [ { name = "pycapnp", specifier = "==2.1.0" }, { name = "pycryptodome" }, { name = "pyjwt" }, - { name = "pyopencl", marker = "extra == 'dev'" }, { name = "pyopenssl", specifier = "<24.3.0" }, - { name = "pyprof2calltree", marker = "extra == 'dev'" }, { name = "pyserial" }, { name = "pytest", marker = "extra == 'testing'" }, { name = "pytest-asyncio", marker = "extra == 'testing'" }, { name = "pytest-cpp", marker = "extra == 'testing'" }, { name = "pytest-mock", marker = "extra == 'testing'" }, - { name = "pytest-randomly", marker = "extra == 'testing'" }, { name = "pytest-repeat", marker = "extra == 'testing'" }, { name = "pytest-subtests", marker = "extra == 'testing'" }, { name = "pytest-timeout", marker = "extra == 'testing'" }, { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, - { name = "pytools", marker = "platform_machine != 'aarch64' and extra == 'dev'", specifier = ">=2025.1.6" }, { name = "pywinctl", marker = "extra == 'dev'" }, { name = "pyzmq" }, { name = "qrcode" }, @@ -1444,15 +1400,12 @@ requires-dist = [ { name = "sentry-sdk" }, { name = "setproctitle" }, { name = "setuptools" }, - { name = "smbus2" }, { name = "sounddevice" }, { name = "spidev", marker = "sys_platform == 'linux'" }, { name = "sympy" }, { name = "tabulate", marker = "extra == 'dev'" }, { name = "tqdm" }, { name = "ty", marker = "extra == 'testing'" }, - { name = "types-requests", marker = "extra == 'dev'" }, - { name = "types-tabulate", marker = "extra == 'dev'" }, { name = "websocket-client" }, { name = "xattr" }, { name = "zstandard" }, @@ -1461,11 +1414,11 @@ provides-extras = ["docs", "testing", "dev", "tools"] [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -1735,11 +1688,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] @@ -4258,34 +4211,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/67/64920c8d201a7fc27962f467c636c4e763b43845baba2e091a50a97a5d52/pyobjc_framework_webkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:af2c7197447638b92aafbe4847c063b6dd5e1ed83b44d3ce7e71e4c9b042ab5a", size = 50084, upload-time = "2025-11-14T10:07:05.868Z" }, ] -[[package]] -name = "pyopencl" -version = "2026.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "platformdirs" }, - { name = "pytools" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d8/81/fd8a2a695916a82e861bcf17b5b8fd9f81e12c9e5931f9ba536678d7b43a/pyopencl-2026.1.2.tar.gz", hash = "sha256:4397dd0b4cbb8b55f3e09bf87114a2465574506b363890b805b860c348b61970", size = 445132, upload-time = "2026-01-16T22:52:24.765Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/88/abf34e31d572c59203774a66cd81c1e3b3d60b911241483675151149c6f1/pyopencl-2026.1.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8052e8b402b3ed33ee0807d87d4734f66f67dbafbfb3f5a8b81e478e4d417372", size = 437029, upload-time = "2026-01-16T22:51:30.953Z" }, - { url = "https://files.pythonhosted.org/packages/5c/3d/2dd2d8bbf05a190681582b40fc1ee55b210d00ccebcbb416c62b9f9c81a1/pyopencl-2026.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5e03681c3fe22d5185b16a727d96783e3787e0b65e7a29e4afe01ae0cb4e802", size = 429031, upload-time = "2026-01-16T22:51:32.674Z" }, - { url = "https://files.pythonhosted.org/packages/41/16/e554b3bd20be2e858cfb6683ee6549aeebbe5f769e5b95f561f79340ab20/pyopencl-2026.1.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c8c209d517d1421b17d20b80589a2c39e09ea33350f0367314e1caeed3bc741", size = 689596, upload-time = "2026-01-16T22:51:33.913Z" }, - { url = "https://files.pythonhosted.org/packages/22/a8/1df41cf6c7b25b3bfda14aa0183c6a90eaf849528ba27753eaa25fb26e20/pyopencl-2026.1.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e64e2e34bcfad426bd24b71fdb6b02aa5cb02475147742fe07ef93e81866fc7e", size = 736427, upload-time = "2026-01-16T22:51:36.595Z" }, - { url = "https://files.pythonhosted.org/packages/fc/3d/177b6a675691f7b6f708faef33f981e72fbc4bfed2b1dfa94dc70d0e8a25/pyopencl-2026.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65b151c56b936481d6b6050c2b9bc520840e1402be78c282ba5c01921c25477d", size = 1163888, upload-time = "2026-01-16T22:51:37.973Z" }, - { url = "https://files.pythonhosted.org/packages/e9/fa/5905571d9fa48827c0427a3e664c0213dd045940d581b3b739d83df9c0f6/pyopencl-2026.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cc40003446037f391ca0970694efb0627e2870fabb20ee21be75bc445a39d8f4", size = 1228235, upload-time = "2026-01-16T22:51:39.786Z" }, - { url = "https://files.pythonhosted.org/packages/1e/3d/538c675d078b91680d8d82962110d0c9fd42e1584763d515d6e2e82d8c57/pyopencl-2026.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:b6a8e109ade7db60e8b1beb48df8f080941d0cd77fb2c225ad509c80cdef603e", size = 474753, upload-time = "2026-01-16T22:51:41.771Z" }, - { url = "https://files.pythonhosted.org/packages/cd/34/1497070e44d1689ddbd01d24a2265910e84ebc53457a489b9d2b6e1ac675/pyopencl-2026.1.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:7d88e59901bfe1f9296fd89acd9968f008dc7cfee7995f8cd09c3f1a77119aa6", size = 438145, upload-time = "2026-01-16T22:51:43.658Z" }, - { url = "https://files.pythonhosted.org/packages/5b/a3/71d6af8741b52d3bef443518c1ccfda003adcfa9cc1d0df83dac7005d08c/pyopencl-2026.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f96a3bff8a09d2fa924e7c33dafac6ea3ef7ec70e746d6d8e17ce2d959a6836", size = 428820, upload-time = "2026-01-16T22:51:45.326Z" }, - { url = "https://files.pythonhosted.org/packages/db/ea/c8dbabeceac9cad3dbb368e08e0aa208cc6c6251c5134cc25eb15da03639/pyopencl-2026.1.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d4e8e8215ec4fdee4b235b61977cdb1c4f041b487bdcf357be799f45b423d61", size = 685478, upload-time = "2026-01-16T22:51:46.545Z" }, - { url = "https://files.pythonhosted.org/packages/64/c7/5854ef7471dfee195bcef6348a107525ca4d1b73c15240e6444d490f9920/pyopencl-2026.1.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0052a8ccbd282d8ab196705e31f4c3ab344113ea5d5c3ddaeede00cdcab068b", size = 734017, upload-time = "2026-01-16T22:51:48.277Z" }, - { url = "https://files.pythonhosted.org/packages/3d/79/42d4eec282ed299b38d8136d05545113ec8771a1bd6b10bb4ba83ae1236c/pyopencl-2026.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e43da12a376e9283407c2820b24cceeaa129b042ac710947cf8e07b13e294689", size = 1159871, upload-time = "2026-01-16T22:51:49.569Z" }, - { url = "https://files.pythonhosted.org/packages/a0/9a/fdc5d3bed0440d6206109e051008aa0a54ca131d64314bbd42177b8f0763/pyopencl-2026.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1b14b2cf11dec9e0b75cbd14223d1b3c93950fc3e2f7a306b54fa1b17a2cae0f", size = 1225288, upload-time = "2026-01-16T22:51:51.125Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e3/358c19180e0dab5c7dd1fcacc569e6a7ab02a7fddcb9c954f393ceddb2fa/pyopencl-2026.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:d02d7ecabc8d34590dccffe12346689adc5a1ceb07df5acc4ea6c4db8aa28277", size = 474876, upload-time = "2026-01-16T22:51:52.912Z" }, -] - [[package]] name = "pyopenssl" version = "24.2.1" @@ -4300,11 +4225,11 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.3.1" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] @@ -4316,12 +4241,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, ] -[[package]] -name = "pyprof2calltree" -version = "1.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/2a/e9a76261183b4b5e059a6625d7aae0bcb0a77622bc767d4497148ce2e218/pyprof2calltree-1.4.5.tar.gz", hash = "sha256:a635672ff31677486350b2be9a823ef92f740e6354a6aeda8fa4a8a3768e8f2f", size = 10080, upload-time = "2020-04-19T10:39:09.819Z" } - [[package]] name = "pyrect" version = "0.2.0" @@ -4399,18 +4318,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] -[[package]] -name = "pytest-randomly" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/1d/258a4bf1109258c00c35043f40433be5c16647387b6e7cd5582d638c116b/pytest_randomly-4.0.1.tar.gz", hash = "sha256:174e57bb12ac2c26f3578188490bd333f0e80620c3f47340158a86eca0593cd8", size = 14130, upload-time = "2025-09-12T15:23:00.085Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/3e/a4a9227807b56869790aad3e24472a554b585974fe7e551ea350f50897ae/pytest_randomly-4.0.1-py3-none-any.whl", hash = "sha256:e0dfad2fd4f35e07beff1e47c17fbafcf98f9bf4531fd369d9260e2f858bfcb7", size = 8304, upload-time = "2025-09-12T15:22:58.946Z" }, -] - [[package]] name = "pytest-repeat" version = "0.9.4" @@ -4487,20 +4394,6 @@ version = "0.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ef/c6/2c5999de3bb1533521f1101e8fe56fd9c266732f4d48011c7c69b29d12ae/python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8", size = 132828, upload-time = "2014-05-31T12:28:59.603Z" } -[[package]] -name = "pytools" -version = "2025.2.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "platformdirs" }, - { name = "siphash24" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/7b/f885a57e61ded45b5b10ca60f0b7575c9fb9a282e7513d0e23a33ee647e1/pytools-2025.2.5.tar.gz", hash = "sha256:a7f5350644d46d98ee9c7e67b4b41693308aa0f5e9b188d8f0694b27dc94e3a2", size = 85594, upload-time = "2025-10-07T15:53:30.49Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/84/c42c29ca4bff35baa286df70b0097e0b1c88fd57e8e6bdb09cb161a6f3c1/pytools-2025.2.5-py3-none-any.whl", hash = "sha256:42e93751ec425781e103bbcd769ba35ecbacd43339c2905401608f2fdc30cf19", size = 98811, upload-time = "2025-10-07T15:53:29.089Z" }, -] - [[package]] name = "pytweening" version = "1.2.0" @@ -4702,28 +4595,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.13" +version = "0.14.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, - { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, - { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, - { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, - { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, - { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] [[package]] @@ -4737,15 +4630,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.49.0" +version = "2.50.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/94/23ac26616a883f492428d9ee9ad6eee391612125326b784dbfc30e1e7bab/sentry_sdk-2.49.0.tar.gz", hash = "sha256:c1878599cde410d481c04ef50ee3aedd4f600e4d0d253f4763041e468b332c30", size = 387228, upload-time = "2026-01-08T09:56:25.642Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/8a/3c4f53d32c21012e9870913544e56bfa9e931aede080779a0f177513f534/sentry_sdk-2.50.0.tar.gz", hash = "sha256:873437a989ee1b8b25579847bae8384515bf18cfed231b06c591b735c1781fe3", size = 401233, upload-time = "2026-01-20T12:53:16.244Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/43/1c586f9f413765201234541857cb82fda076f4b0f7bad4a0ec248da39cf3/sentry_sdk-2.49.0-py2.py3-none-any.whl", hash = "sha256:6ea78499133874445a20fe9c826c9e960070abeb7ae0cdf930314ab16bb97aa0", size = 415693, upload-time = "2026-01-08T09:56:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5b/cbc2bb9569f03c8e15d928357e7e6179e5cfab45544a3bbac8aec4caf9be/sentry_sdk-2.50.0-py2.py3-none-any.whl", hash = "sha256:0ef0ed7168657ceb5a0be081f4102d92042a125462d1d1a29277992e344e749e", size = 424961, upload-time = "2026-01-20T12:53:14.826Z" }, ] [[package]] @@ -4781,11 +4674,11 @@ wheels = [ [[package]] name = "setuptools" -version = "80.9.0" +version = "80.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, ] [[package]] @@ -4815,26 +4708,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, ] -[[package]] -name = "siphash24" -version = "1.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/a2/e049b6fccf7a94bd1b2f68b3059a7d6a7aea86a808cac80cb9ae71ab6254/siphash24-1.8.tar.gz", hash = "sha256:aa932f0af4a7335caef772fdaf73a433a32580405c41eb17ff24077944b0aa97", size = 19946, upload-time = "2025-09-02T20:42:04.856Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/23/f53f5bd8866c6ea3abe434c9f208e76ea027210d8b75cd0e0dc849661c7a/siphash24-1.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4662ac616bce4d3c9d6003a0d398e56f8be408fc53a166b79fad08d4f34268e", size = 76930, upload-time = "2025-09-02T20:41:00.869Z" }, - { url = "https://files.pythonhosted.org/packages/0b/25/aebf246904424a06e7ffb7a40cfa9ea9e590ea0fac82e182e0f5d1f1d7ef/siphash24-1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:53d6bed0951a99c6d2891fa6f8acfd5ca80c3e96c60bcee99f6fa01a04773b1c", size = 74315, upload-time = "2025-09-02T20:41:02.38Z" }, - { url = "https://files.pythonhosted.org/packages/59/3f/7010407c3416ef052d46550d54afb2581fb247018fc6500af8c66669eff2/siphash24-1.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d114c03648630e9e07dac2fe95442404e4607adca91640d274ece1a4fa71123e", size = 99756, upload-time = "2025-09-02T20:41:03.902Z" }, - { url = "https://files.pythonhosted.org/packages/d4/9f/09c734833e69badd7e3faed806b4372bd6564ae0946bd250d5239885914f/siphash24-1.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88c1a55ff82b127c5d3b96927a430d8859e6a98846a5b979833ac790682dd91b", size = 104044, upload-time = "2025-09-02T20:41:05.505Z" }, - { url = "https://files.pythonhosted.org/packages/24/30/56a26d9141a34433da221f732599e2b23d2d70a966c249a9f00feb9a2915/siphash24-1.8-cp311-cp311-win32.whl", hash = "sha256:9430255e6a1313470f52c07c4a4643c451a5b2853f6d4008e4dda05cafb6ce7c", size = 62196, upload-time = "2025-09-02T20:41:07.299Z" }, - { url = "https://files.pythonhosted.org/packages/47/b2/11b0ae63fd374652544e1b12f72ba2cc3fe6c93c1483bd8ff6935b0a8a4b/siphash24-1.8-cp311-cp311-win_amd64.whl", hash = "sha256:1e4b37e4ef0b4496169adce2a58b6c3f230b5852dfa5f7ad0b2d664596409e47", size = 77162, upload-time = "2025-09-02T20:41:08.878Z" }, - { url = "https://files.pythonhosted.org/packages/7f/82/ce3545ce8052ac7ca104b183415a27ec3335e5ed51978fdd7b433f3cfe5b/siphash24-1.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5ed437c6e6cc96196b38728e57cd30b0427df45223475a90e173f5015ef5ba", size = 78136, upload-time = "2025-09-02T20:41:10.083Z" }, - { url = "https://files.pythonhosted.org/packages/15/88/896c3b91bc9deb78c415448b1db67343917f35971a9e23a5967a9d323b8a/siphash24-1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4ef78abdf811325c7089a35504df339c48c0007d4af428a044431d329721e56", size = 74588, upload-time = "2025-09-02T20:41:11.251Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/8dad3f5601db485ba862e1c1f91a5d77fb563650856a6708e9acb40ee53c/siphash24-1.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:065eff55c4fefb3a29fd26afb2c072abf7f668ffd53b91d41f92a1c485fcbe5c", size = 98655, upload-time = "2025-09-02T20:41:12.45Z" }, - { url = "https://files.pythonhosted.org/packages/e3/cc/e0c352624c1f2faad270aeb5cce6e173977ef66b9b5e918aa6f32af896bf/siphash24-1.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6fa84ebfd47677262aa0bcb0f5a70f796f5fc5704b287ee1b65a3bd4fb7a5d", size = 103217, upload-time = "2025-09-02T20:41:13.746Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f6/0b1675bea4d40affcae642d9c7337702a4138b93c544230280712403e968/siphash24-1.8-cp312-cp312-win32.whl", hash = "sha256:6582f73615552ca055e51e03cb02a28e570a641a7f500222c86c2d811b5037eb", size = 63114, upload-time = "2025-09-02T20:41:14.972Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/afefef85d72ed8b5cf1aa9283f712e3cd43c9682fabbc809dec54baa8452/siphash24-1.8-cp312-cp312-win_amd64.whl", hash = "sha256:44ea6d794a7cbe184e1e1da2df81c5ebb672ab3867935c3e87c08bb0c2fa4879", size = 76232, upload-time = "2025-09-02T20:41:16.112Z" }, -] - [[package]] name = "six" version = "1.17.0" @@ -4844,15 +4717,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "smbus2" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/36/afafd43770caae69f04e21402552a8f94a072def46a002fab9357f4852ce/smbus2-0.6.0.tar.gz", hash = "sha256:9b5ff1e998e114730f9dfe0c4babbef06c92468cfb61eaa684e30f225661b95b", size = 17403, upload-time = "2025-12-20T09:02:52.017Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/cf/2e1d6805da6f9c9b3a4358076ff2e072d828ba7fed124edc1b729e210c55/smbus2-0.6.0-py2.py3-none-any.whl", hash = "sha256:03d83d2a9a4afc5ddca0698ccabf101cb3de52bc5aefd7b76778ffb27ff654e0", size = 11849, upload-time = "2025-12-20T09:02:51.219Z" }, -] - [[package]] name = "sortedcontainers" version = "2.4.0" @@ -4864,17 +4728,18 @@ wheels = [ [[package]] name = "sounddevice" -version = "0.5.3" +version = "0.5.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/4f/28e734898b870db15b6474453f19813d3c81b91c806d9e6f867bd6e4dd03/sounddevice-0.5.3.tar.gz", hash = "sha256:cbac2b60198fbab84533697e7c4904cc895ec69d5fb3973556c9eb74a4629b2c", size = 53465, upload-time = "2025-10-19T13:23:57.922Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/f9/2592608737553638fca98e21e54bfec40bf577bb98a61b2770c912aab25e/sounddevice-0.5.5.tar.gz", hash = "sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3", size = 143191, upload-time = "2026-01-23T18:36:43.529Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/e7/9020e9f0f3df00432728f4c4044387468a743e3d9a4f91123d77be10010e/sounddevice-0.5.3-py3-none-any.whl", hash = "sha256:ea7738baa0a9f9fef7390f649e41c9f2c8ada776180e56c2ffd217133c92a806", size = 32670, upload-time = "2025-10-19T13:23:51.779Z" }, - { url = "https://files.pythonhosted.org/packages/2f/39/714118f8413e0e353436914f2b976665161f1be2b6483ac15a8f61484c14/sounddevice-0.5.3-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:278dc4451fff70934a176df048b77d80d7ce1623a6ec9db8b34b806f3112f9c2", size = 108306, upload-time = "2025-10-19T13:23:53.277Z" }, - { url = "https://files.pythonhosted.org/packages/f5/74/52186e3e5c833d00273f7949a9383adff93692c6e02406bf359cb4d3e921/sounddevice-0.5.3-py3-none-win32.whl", hash = "sha256:845d6927bcf14e84be5292a61ab3359cf8e6b9145819ec6f3ac2619ff089a69c", size = 312882, upload-time = "2025-10-19T13:23:54.829Z" }, - { url = "https://files.pythonhosted.org/packages/66/c7/16123d054aef6d445176c9122bfbe73c11087589b2413cab22aff5a7839a/sounddevice-0.5.3-py3-none-win_amd64.whl", hash = "sha256:f55ad20082efc2bdec06928e974fbcae07bc6c405409ae1334cefe7d377eb687", size = 364025, upload-time = "2025-10-19T13:23:56.362Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl", hash = "sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f", size = 32807, upload-time = "2026-01-23T18:36:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722", size = 108557, upload-time = "2026-01-23T18:36:37.41Z" }, + { url = "https://files.pythonhosted.org/packages/88/a1/d19dd9889cd4bce2e233c4fac007cd8daaf5b9fe6e6a5d432cf17be0b807/sounddevice-0.5.5-py3-none-win32.whl", hash = "sha256:1234cc9b4c9df97b6cbe748146ae0ec64dd7d6e44739e8e42eaa5b595313a103", size = 317765, upload-time = "2026-01-23T18:36:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0e/002ed7c4c1c2ab69031f78989d3b789fee3a7fba9e586eb2b81688bf4961/sounddevice-0.5.5-py3-none-win_amd64.whl", hash = "sha256:cfc6b2c49fb7f555591c78cb8ecf48d6a637fd5b6e1db5fec6ed9365d64b3519", size = 365324, upload-time = "2026-01-23T18:36:40.496Z" }, + { url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" }, ] [[package]] @@ -4918,48 +4783,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.12" +version = "0.0.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/78/ba1a4ad403c748fbba8be63b7e774a90e80b67192f6443d624c64fe4aaab/ty-0.0.12.tar.gz", hash = "sha256:cd01810e106c3b652a01b8f784dd21741de9fdc47bd595d02c122a7d5cefeee7", size = 4981303, upload-time = "2026-01-14T22:30:48.537Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/dc/b607f00916f5a7c52860b84a66dc17bc6988e8445e96b1d6e175a3837397/ty-0.0.13.tar.gz", hash = "sha256:7a1d135a400ca076407ea30012d1f75419634160ed3b9cad96607bf2956b23b3", size = 4999183, upload-time = "2026-01-21T13:21:16.133Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/8f/c21314d074dda5fb13d3300fa6733fd0d8ff23ea83a721818740665b6314/ty-0.0.12-py3-none-linux_armv6l.whl", hash = "sha256:eb9da1e2c68bd754e090eab39ed65edf95168d36cbeb43ff2bd9f86b4edd56d1", size = 9614164, upload-time = "2026-01-14T22:30:44.016Z" }, - { url = "https://files.pythonhosted.org/packages/09/28/f8a4d944d13519d70c486e8f96d6fa95647ac2aa94432e97d5cfec1f42f6/ty-0.0.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c181f42aa19b0ed7f1b0c2d559980b1f1d77cc09419f51c8321c7ddf67758853", size = 9542337, upload-time = "2026-01-14T22:30:05.687Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9c/f576e360441de7a8201daa6dc4ebc362853bc5305e059cceeb02ebdd9a48/ty-0.0.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1f829e1eecd39c3e1b032149db7ae6a3284f72fc36b42436e65243a9ed1173db", size = 8909582, upload-time = "2026-01-14T22:30:46.089Z" }, - { url = "https://files.pythonhosted.org/packages/d6/13/0898e494032a5d8af3060733d12929e3e7716db6c75eac63fa125730a3e7/ty-0.0.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45162e7826e1789cf3374627883cdeb0d56b82473a0771923e4572928e90be3", size = 9384932, upload-time = "2026-01-14T22:30:13.769Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1a/b35b6c697008a11d4cedfd34d9672db2f0a0621ec80ece109e13fca4dfef/ty-0.0.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d11fec40b269bec01e751b2337d1c7ffa959a2c2090a950d7e21c2792442cccd", size = 9453140, upload-time = "2026-01-14T22:30:11.131Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1e/71c9edbc79a3c88a0711324458f29c7dbf6c23452c6e760dc25725483064/ty-0.0.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09d99e37e761a4d2651ad9d5a610d11235fbcbf35dc6d4bc04abf54e7cf894f1", size = 9960680, upload-time = "2026-01-14T22:30:33.621Z" }, - { url = "https://files.pythonhosted.org/packages/0e/75/39375129f62dd22f6ad5a99cd2a42fd27d8b91b235ce2db86875cdad397d/ty-0.0.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d9ca0cdb17bd37397da7b16a7cd23423fc65c3f9691e453ad46c723d121225a1", size = 10904518, upload-time = "2026-01-14T22:30:08.464Z" }, - { url = "https://files.pythonhosted.org/packages/32/5e/26c6d88fafa11a9d31ca9f4d12989f57782ec61e7291d4802d685b5be118/ty-0.0.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcf2757b905e7eddb7e456140066335b18eb68b634a9f72d6f54a427ab042c64", size = 10525001, upload-time = "2026-01-14T22:30:16.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a5/2f0b91894af13187110f9ad7ee926d86e4e6efa755c9c88a820ed7f84c85/ty-0.0.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00cf34c1ebe1147efeda3021a1064baa222c18cdac114b7b050bbe42deb4ca80", size = 10307103, upload-time = "2026-01-14T22:30:41.221Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/13d0410827e4bc713ebb7fdaf6b3590b37dcb1b82e0a81717b65548f2442/ty-0.0.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb3a655bd869352e9a22938d707631ac9fbca1016242b1f6d132d78f347c851", size = 10072737, upload-time = "2026-01-14T22:30:51.783Z" }, - { url = "https://files.pythonhosted.org/packages/e1/dd/fc36d8bac806c74cf04b4ca735bca14d19967ca84d88f31e121767880df1/ty-0.0.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4658e282c7cb82be304052f8f64f9925f23c3c4f90eeeb32663c74c4b095d7ba", size = 9368726, upload-time = "2026-01-14T22:30:18.683Z" }, - { url = "https://files.pythonhosted.org/packages/54/70/9e8e461647550f83e2fe54bc632ccbdc17a4909644783cdbdd17f7296059/ty-0.0.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c167d838eaaa06e03bb66a517f75296b643d950fbd93c1d1686a187e5a8dbd1f", size = 9454704, upload-time = "2026-01-14T22:30:22.759Z" }, - { url = "https://files.pythonhosted.org/packages/04/9b/6292cf7c14a0efeca0539cf7d78f453beff0475cb039fbea0eb5d07d343d/ty-0.0.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2956e0c9ab7023533b461d8a0e6b2ea7b78e01a8dde0688e8234d0fce10c4c1c", size = 9649829, upload-time = "2026-01-14T22:30:31.234Z" }, - { url = "https://files.pythonhosted.org/packages/49/bd/472a5d2013371e4870886cff791c94abdf0b92d43d305dd0f8e06b6ff719/ty-0.0.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c6a3fd7479580009f21002f3828320621d8a82d53b7ba36993234e3ccad58c8", size = 10162814, upload-time = "2026-01-14T22:30:36.174Z" }, - { url = "https://files.pythonhosted.org/packages/31/e9/2ecbe56826759845a7c21d80aa28187865ea62bc9757b056f6cbc06f78ed/ty-0.0.12-py3-none-win32.whl", hash = "sha256:a91c24fd75c0f1796d8ede9083e2c0ec96f106dbda73a09fe3135e075d31f742", size = 9140115, upload-time = "2026-01-14T22:30:38.903Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6d/d9531eff35a5c0ec9dbc10231fac21f9dd6504814048e81d6ce1c84dc566/ty-0.0.12-py3-none-win_amd64.whl", hash = "sha256:df151894be55c22d47068b0f3b484aff9e638761e2267e115d515fcc9c5b4a4b", size = 9884532, upload-time = "2026-01-14T22:30:25.112Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f3/20b49e75967023b123a221134548ad7000f9429f13fdcdda115b4c26305f/ty-0.0.12-py3-none-win_arm64.whl", hash = "sha256:cea99d334b05629de937ce52f43278acf155d3a316ad6a35356635f886be20ea", size = 9313974, upload-time = "2026-01-14T22:30:27.44Z" }, -] - -[[package]] -name = "types-requests" -version = "2.32.4.20260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, -] - -[[package]] -name = "types-tabulate" -version = "0.9.0.20241207" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/43/16030404a327e4ff8c692f2273854019ed36718667b2993609dc37d14dd4/types_tabulate-0.9.0.20241207.tar.gz", hash = "sha256:ac1ac174750c0a385dfd248edc6279fa328aaf4ea317915ab879a2ec47833230", size = 8195, upload-time = "2024-12-07T02:54:42.554Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/86/a9ebfd509cbe74471106dffed320e208c72537f9aeb0a55eaa6b1b5e4d17/types_tabulate-0.9.0.20241207-py3-none-any.whl", hash = "sha256:b8dad1343c2a8ba5861c5441370c3e35908edd234ff036d4298708a1d4cf8a85", size = 8307, upload-time = "2024-12-07T02:54:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/3632f1918f4c0a33184f107efc5d436ab6da147fd3d3b94b3af6461efbf4/ty-0.0.13-py3-none-linux_armv6l.whl", hash = "sha256:1b2b8e02697c3a94c722957d712a0615bcc317c9b9497be116ef746615d892f2", size = 9993501, upload-time = "2026-01-21T13:21:26.628Z" }, + { url = "https://files.pythonhosted.org/packages/92/87/6a473ced5ac280c6ce5b1627c71a8a695c64481b99aabc798718376a441e/ty-0.0.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f15cdb8e233e2b5adfce673bb21f4c5e8eaf3334842f7eea3c70ac6fda8c1de5", size = 9860986, upload-time = "2026-01-21T13:21:24.425Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9b/d89ae375cf0a7cd9360e1164ce017f8c753759be63b6a11ed4c944abe8c6/ty-0.0.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0819e89ac9f0d8af7a062837ce197f0461fee2fc14fd07e2c368780d3a397b73", size = 9350748, upload-time = "2026-01-21T13:21:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a6/9ad58518056fab344b20c0bb2c1911936ebe195318e8acc3bc45ac1c6b6b/ty-0.0.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de79f481084b7cc7a202ba0d7a75e10970d10ffa4f025b23f2e6b7324b74886", size = 9849884, upload-time = "2026-01-21T13:21:21.886Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c3/8add69095fa179f523d9e9afcc15a00818af0a37f2b237a9b59bc0046c34/ty-0.0.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4fb2154cff7c6e95d46bfaba283c60642616f20d73e5f96d0c89c269f3e1bcec", size = 9822975, upload-time = "2026-01-21T13:21:14.292Z" }, + { url = "https://files.pythonhosted.org/packages/a4/05/4c0927c68a0a6d43fb02f3f0b6c19c64e3461dc8ed6c404dde0efb8058f7/ty-0.0.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00be58d89337c27968a20d58ca553458608c5b634170e2bec82824c2e4cf4d96", size = 10294045, upload-time = "2026-01-21T13:21:30.505Z" }, + { url = "https://files.pythonhosted.org/packages/b4/86/6dc190838aba967557fe0bfd494c595d00b5081315a98aaf60c0e632aaeb/ty-0.0.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72435eade1fa58c6218abb4340f43a6c3ff856ae2dc5722a247d3a6dd32e9737", size = 10916460, upload-time = "2026-01-21T13:21:07.788Z" }, + { url = "https://files.pythonhosted.org/packages/04/40/9ead96b7c122e1109dfcd11671184c3506996bf6a649306ec427e81d9544/ty-0.0.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77a548742ee8f621d718159e7027c3b555051d096a49bb580249a6c5fc86c271", size = 10597154, upload-time = "2026-01-21T13:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7d/e832a2c081d2be845dc6972d0c7998914d168ccbc0b9c86794419ab7376e/ty-0.0.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da067c57c289b7cf914669704b552b6207c2cc7f50da4118c3e12388642e6b3f", size = 10410710, upload-time = "2026-01-21T13:21:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/31/e3/898be3a96237a32f05c4c29b43594dc3b46e0eedfe8243058e46153b324f/ty-0.0.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d1b50a01fffa140417fca5a24b658fbe0734074a095d5b6f0552484724474343", size = 9826299, upload-time = "2026-01-21T13:21:00.845Z" }, + { url = "https://files.pythonhosted.org/packages/bb/eb/db2d852ce0ed742505ff18ee10d7d252f3acfd6fc60eca7e9c7a0288a6d8/ty-0.0.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f33c46f52e5e9378378eca0d8059f026f3c8073ace02f7f2e8d079ddfe5207e", size = 9831610, upload-time = "2026-01-21T13:21:05.842Z" }, + { url = "https://files.pythonhosted.org/packages/9e/61/149f59c8abaddcbcbb0bd13b89c7741ae1c637823c5cf92ed2c644fcadef/ty-0.0.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:168eda24d9a0b202cf3758c2962cc295878842042b7eca9ed2965259f59ce9f2", size = 9978885, upload-time = "2026-01-21T13:21:10.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/026d4e4af60a80918a8d73d2c42b8262dd43ab2fa7b28d9743004cb88d57/ty-0.0.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4917678b95dc8cb399cc459fab568ba8d5f0f33b7a94bf840d9733043c43f29", size = 10506453, upload-time = "2026-01-21T13:20:56.633Z" }, + { url = "https://files.pythonhosted.org/packages/63/06/8932833a4eca2df49c997a29afb26721612de8078ae79074c8fe87e17516/ty-0.0.13-py3-none-win32.whl", hash = "sha256:c1f2ec40daa405508b053e5b8e440fbae5fdb85c69c9ab0ee078f8bc00eeec3d", size = 9433482, upload-time = "2026-01-21T13:20:58.717Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fd/e8d972d1a69df25c2cecb20ea50e49ad5f27a06f55f1f5f399a563e71645/ty-0.0.13-py3-none-win_amd64.whl", hash = "sha256:8b7b1ab9f187affbceff89d51076038363b14113be29bda2ddfa17116de1d476", size = 10319156, upload-time = "2026-01-21T13:21:03.266Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c2/05fdd64ac003a560d4fbd1faa7d9a31d75df8f901675e5bed1ee2ceeff87/ty-0.0.13-py3-none-win_arm64.whl", hash = "sha256:1c9630333497c77bb9bcabba42971b96ee1f36c601dd3dcac66b4134f9fa38f0", size = 9808316, upload-time = "2026-01-21T13:20:54.053Z" }, ] [[package]]