Merge branch 'master' into os_10x2

pull/32112/head
ZwX1616 1 year ago
commit eab6dcb06e
  1. 3
      .github/workflows/stale.yaml
  2. 2
      .github/workflows/tools_tests.yaml
  3. 4
      .pre-commit-config.yaml
  4. 25
      Jenkinsfile
  5. 2
      cereal
  6. 30
      common/git.py
  7. 4
      common/prefix.py
  8. 8
      common/run.py
  9. 13
      common/time.py
  10. 5
      common/util.cc
  11. 2
      common/util.h
  12. 8
      docs/CARS.md
  13. 2
      opendbc
  14. 2
      panda
  15. 674
      poetry.lock
  16. 44
      release/README.md
  17. 15
      release/copy_build_files.sh
  18. 22
      release/create_casync_build.sh
  19. 28
      release/create_casync_release.py
  20. 34
      release/create_prebuilt.sh
  21. 5
      release/files_common
  22. 9
      release/upload_casync_release.sh
  23. 12
      selfdrive/athena/athenad.py
  24. 15
      selfdrive/athena/manage_athenad.py
  25. 3
      selfdrive/boardd/boardd.cc
  26. 2
      selfdrive/boardd/panda.cc
  27. 2
      selfdrive/boardd/panda.h
  28. 5
      selfdrive/boardd/pandad.py
  29. 2
      selfdrive/boardd/tests/test_boardd_usbprotocol.cc
  30. 36
      selfdrive/boardd/tests/test_pandad.py
  31. 14
      selfdrive/car/__init__.py
  32. 5
      selfdrive/car/car_helpers.py
  33. 16
      selfdrive/car/chrysler/fingerprints.py
  34. 2
      selfdrive/car/docs_definitions.py
  35. 6
      selfdrive/car/fingerprints.py
  36. 7
      selfdrive/car/ford/carstate.py
  37. 2
      selfdrive/car/ford/fingerprints.py
  38. 3
      selfdrive/car/fw_versions.py
  39. 2
      selfdrive/car/honda/fingerprints.py
  40. 51
      selfdrive/car/honda/values.py
  41. 668
      selfdrive/car/hyundai/fingerprints.py
  42. 18
      selfdrive/car/hyundai/tests/test_hyundai.py
  43. 66
      selfdrive/car/hyundai/values.py
  44. 3
      selfdrive/car/mazda/fingerprints.py
  45. 8
      selfdrive/car/tests/test_fw_fingerprint.py
  46. 5
      selfdrive/car/tests/test_models.py
  47. 3
      selfdrive/car/toyota/fingerprints.py
  48. 8
      selfdrive/car/values.py
  49. 27
      selfdrive/car/volkswagen/carstate.py
  50. 6
      selfdrive/car/volkswagen/fingerprints.py
  51. 5
      selfdrive/car/volkswagen/values.py
  52. 4
      selfdrive/controls/controlsd.py
  53. 5
      selfdrive/debug/check_can_parser_performance.py
  54. 2
      selfdrive/debug/cycle_alerts.py
  55. 5
      selfdrive/manager/build.py
  56. 41
      selfdrive/manager/manager.py
  57. 15
      selfdrive/navd/navd.py
  58. 18
      selfdrive/sentry.py
  59. 12
      selfdrive/statsd.py
  60. 2
      selfdrive/test/process_replay/ref_commit
  61. 15
      selfdrive/thermald/thermald.py
  62. 6
      selfdrive/tombstoned.py
  63. 8
      selfdrive/ui/qt/maps/map_helpers.cc
  64. 36
      selfdrive/ui/qt/offroad/settings.cc
  65. 4
      selfdrive/ui/qt/offroad/settings.h
  66. 84
      selfdrive/ui/qt/onroad.cc
  67. 24
      selfdrive/ui/qt/onroad.h
  68. 2
      selfdrive/ui/qt/sidebar.cc
  69. 21
      selfdrive/ui/qt/util.cc
  70. 2
      selfdrive/ui/qt/widgets/cameraview.cc
  71. 51
      selfdrive/ui/translations/main_ar.ts
  72. 39
      selfdrive/ui/translations/main_de.ts
  73. 39
      selfdrive/ui/translations/main_fr.ts
  74. 39
      selfdrive/ui/translations/main_ja.ts
  75. 41
      selfdrive/ui/translations/main_ko.ts
  76. 39
      selfdrive/ui/translations/main_pt-BR.ts
  77. 39
      selfdrive/ui/translations/main_th.ts
  78. 39
      selfdrive/ui/translations/main_tr.ts
  79. 39
      selfdrive/ui/translations/main_zh-CHS.ts
  80. 39
      selfdrive/ui/translations/main_zh-CHT.ts
  81. 60
      selfdrive/ui/ui.h
  82. 5
      selfdrive/updated/updated.py
  83. 2
      system/hardware/tici/agnos.py
  84. 4
      system/timed.py
  85. 50
      system/updated/casync/casync.py
  86. 55
      system/updated/casync/common.py
  87. 38
      system/updated/casync/tar.py
  88. 116
      system/updated/casync/tests/test_casync.py
  89. 138
      system/version.py
  90. 2
      teleoprtc_repo
  91. 1
      third_party/libyuv/.gitignore
  92. 31
      third_party/libyuv/build.sh
  93. 12
      third_party/maplibre-native-qt/build.sh
  94. 12
      tools/cabana/dbc/dbcfile.cc
  95. 10
      tools/cabana/dbc/generate_dbc_json.py
  96. 21
      tools/cabana/tests/test_cabana.cc
  97. 304
      tools/foxglove/fox.py
  98. 3
      tools/foxglove/install_foxglove.sh
  99. 1
      tools/install_ubuntu_dependencies.sh
  100. 10
      tools/lib/helpers.py
  101. Some files were not shown because too many files have changed in this diff Show More

@ -19,8 +19,9 @@ jobs:
# pull request config # pull request config
stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.' stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.'
close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.' close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.'
stale-pr-label: stale
delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'commaai/openpilot' }} # only delete branches on the main repo delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'commaai/openpilot' }} # only delete branches on the main repo
exempt-pr-labels: "ignore stale,needs testing,car port" # if wip or it needs testing from the community, don't mark as stale exempt-pr-labels: "ignore stale,needs testing,car port,car" # if wip or it needs testing from the community, don't mark as stale
days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }} days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }}
days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }}

@ -31,7 +31,7 @@ jobs:
- uses: ./.github/workflows/setup-with-retry - uses: ./.github/workflows/setup-with-retry
- name: Build openpilot - name: Build openpilot
timeout-minutes: 5 timeout-minutes: 5
run: ${{ env.RUN }} "scons -j$(nproc) cereal/ common/ --minimal" run: ${{ env.RUN }} "scons -j$(nproc) cereal/ common/ opendbc/ --minimal"
- name: Test PlotJuggler - name: Test PlotJuggler
timeout-minutes: 2 timeout-minutes: 2
run: | run: |

@ -33,7 +33,7 @@ repos:
- -L bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints - -L bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints
- --builtins clear,rare,informal,usage,code,names,en-GB_to_en-US - --builtins clear,rare,informal,usage,code,names,en-GB_to_en-US
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.3 rev: v0.3.4
hooks: hooks:
- id: ruff - id: ruff
exclude: '^(third_party/)|(cereal/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)' exclude: '^(third_party/)|(cereal/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)'
@ -98,6 +98,6 @@ repos:
args: args:
- --lock - --lock
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.28.0 rev: 0.28.1
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows

25
Jenkinsfile vendored

@ -142,6 +142,23 @@ def setupCredentials() {
} }
def build_release(String channel_name) {
return parallel (
"${channel_name} (git)": {
deviceStage("build git", "tici-needs-can", [], [
["build ${channel_name}", "RELEASE_BRANCH=${channel_name} $SOURCE_DIR/release/build_release.sh"],
])
},
"${channel_name} (casync)": {
deviceStage("build casync", "tici-needs-can", [], [
["build ${channel_name}", "RELEASE=1 OPENPILOT_CHANNEL=${channel_name} BUILD_DIR=/data/openpilot CASYNC_DIR=/data/casync $SOURCE_DIR/release/create_casync_build.sh"],
//["upload ${channel_name}", "OPENPILOT_CHANNEL=${channel_name} $SOURCE_DIR/release/upload_casync_release.sh"],
])
}
)
}
node { node {
env.CI = "1" env.CI = "1"
env.PYTHONWARNINGS = "error" env.PYTHONWARNINGS = "error"
@ -164,15 +181,11 @@ node {
try { try {
if (env.BRANCH_NAME == 'devel-staging') { if (env.BRANCH_NAME == 'devel-staging') {
deviceStage("build release3-staging", "tici-needs-can", [], [ build_release("release3-staging")
["build release3-staging", "RELEASE_BRANCH=release3-staging $SOURCE_DIR/release/build_release.sh"],
])
} }
if (env.BRANCH_NAME == 'master-ci') { if (env.BRANCH_NAME == 'master-ci') {
deviceStage("build nightly", "tici-needs-can", [], [ build_release("nightly")
["build nightly", "RELEASE_BRANCH=nightly $SOURCE_DIR/release/build_release.sh"],
])
} }
if (!env.BRANCH_NAME.matches(excludeRegex)) { if (!env.BRANCH_NAME.matches(excludeRegex)) {

@ -1 +1 @@
Subproject commit 430535068ac3bb94d3e117a3cfbc348ef37eb72d Subproject commit b9871482a3cba70c2ab40ab80017019313dc3f31

@ -4,38 +4,38 @@ from openpilot.common.run import run_cmd, run_cmd_default
@cache @cache
def get_commit(branch: str = "HEAD") -> str: def get_commit(cwd: str = None, branch: str = "HEAD") -> str:
return run_cmd_default(["git", "rev-parse", branch]) return run_cmd_default(["git", "rev-parse", branch], cwd=cwd)
@cache @cache
def get_commit_date(commit: str = "HEAD") -> str: def get_commit_date(cwd: str = None, commit: str = "HEAD") -> str:
return run_cmd_default(["git", "show", "--no-patch", "--format='%ct %ci'", commit]) return run_cmd_default(["git", "show", "--no-patch", "--format='%ct %ci'", commit], cwd=cwd)
@cache @cache
def get_short_branch() -> str: def get_short_branch(cwd: str = None) -> str:
return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "HEAD"]) return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd)
@cache @cache
def get_branch() -> str: def get_branch(cwd: str = None) -> str:
return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]) return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd=cwd)
@cache @cache
def get_origin() -> str: def get_origin(cwd: str = None) -> str:
try: try:
local_branch = run_cmd(["git", "name-rev", "--name-only", "HEAD"]) local_branch = run_cmd(["git", "name-rev", "--name-only", "HEAD"], cwd=cwd)
tracking_remote = run_cmd(["git", "config", "branch." + local_branch + ".remote"]) tracking_remote = run_cmd(["git", "config", "branch." + local_branch + ".remote"], cwd=cwd)
return run_cmd(["git", "config", "remote." + tracking_remote + ".url"]) return run_cmd(["git", "config", "remote." + tracking_remote + ".url"], cwd=cwd)
except subprocess.CalledProcessError: # Not on a branch, fallback except subprocess.CalledProcessError: # Not on a branch, fallback
return run_cmd_default(["git", "config", "--get", "remote.origin.url"]) return run_cmd_default(["git", "config", "--get", "remote.origin.url"], cwd=cwd)
@cache @cache
def get_normalized_origin() -> str: def get_normalized_origin(cwd: str = None) -> str:
return get_origin() \ return get_origin(cwd) \
.replace("git@", "", 1) \ .replace("git@", "", 1) \
.replace(".git", "", 1) \ .replace(".git", "", 1) \
.replace("https://", "", 1) \ .replace("https://", "", 1) \

@ -4,6 +4,7 @@ import uuid
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.hardware import PC
from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import Paths
from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
@ -45,7 +46,8 @@ class OpenpilotPrefix:
shutil.rmtree(os.path.realpath(symlink_path), ignore_errors=True) shutil.rmtree(os.path.realpath(symlink_path), ignore_errors=True)
os.remove(symlink_path) os.remove(symlink_path)
shutil.rmtree(self.msgq_path, ignore_errors=True) shutil.rmtree(self.msgq_path, ignore_errors=True)
shutil.rmtree(Paths.log_root(), ignore_errors=True) if PC:
shutil.rmtree(Paths.log_root(), ignore_errors=True)
if not os.environ.get("COMMA_CACHE", False): if not os.environ.get("COMMA_CACHE", False):
shutil.rmtree(Paths.download_cache_root(), ignore_errors=True) shutil.rmtree(Paths.download_cache_root(), ignore_errors=True)
shutil.rmtree(Paths.comma_home(), ignore_errors=True) shutil.rmtree(Paths.comma_home(), ignore_errors=True)

@ -1,13 +1,13 @@
import subprocess import subprocess
def run_cmd(cmd: list[str]) -> str: def run_cmd(cmd: list[str], cwd=None, env=None) -> str:
return subprocess.check_output(cmd, encoding='utf8').strip() return subprocess.check_output(cmd, encoding='utf8', cwd=cwd, env=env).strip()
def run_cmd_default(cmd: list[str], default: str = "") -> str: def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> str:
try: try:
return run_cmd(cmd) return run_cmd(cmd, cwd=cwd, env=env)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
return default return default

@ -1,6 +1,15 @@
import datetime import datetime
from pathlib import Path
MIN_DATE = datetime.datetime(year=2024, month=1, day=28) _MIN_DATE = datetime.datetime(year=2024, month=3, day=30)
def min_date():
# on systemd systems, the default time is the systemd build time
systemd_path = Path("/lib/systemd/systemd")
if systemd_path.exists():
d = datetime.datetime.fromtimestamp(systemd_path.stat().st_mtime)
return d + datetime.timedelta(days=1)
return _MIN_DATE
def system_time_valid(): def system_time_valid():
return datetime.datetime.now() > MIN_DATE return datetime.datetime.now() > min_date()

@ -256,8 +256,9 @@ bool starts_with(const std::string &s1, const std::string &s2) {
return strncmp(s1.c_str(), s2.c_str(), s2.size()) == 0; return strncmp(s1.c_str(), s2.c_str(), s2.size()) == 0;
} }
bool ends_with(const std::string &s1, const std::string &s2) { bool ends_with(const std::string& s, const std::string& suffix) {
return strcmp(s1.c_str() + (s1.size() - s2.size()), s2.c_str()) == 0; return s.size() >= suffix.size() &&
strcmp(s.c_str() + (s.size() - suffix.size()), suffix.c_str()) == 0;
} }
std::string check_output(const std::string& command) { std::string check_output(const std::string& command) {

@ -77,7 +77,7 @@ float getenv(const char* key, float default_val);
std::string hexdump(const uint8_t* in, const size_t size); std::string hexdump(const uint8_t* in, const size_t size);
std::string dir_name(std::string const& path); std::string dir_name(std::string const& path);
bool starts_with(const std::string &s1, const std::string &s2); bool starts_with(const std::string &s1, const std::string &s2);
bool ends_with(const std::string &s1, const std::string &s2); bool ends_with(const std::string &s, const std::string &suffix);
// ***** random helpers ***** // ***** random helpers *****
int random_int(int min, int max); int random_int(int min, int max);

@ -4,7 +4,7 @@
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. 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.
# 289 Supported Cars # 291 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video| |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@ -51,7 +51,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2022">Buy Here</a></sub></details>|| |Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2022">Buy Here</a></sub></details>||
|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2023-24">Buy Here</a></sub></details>|| |Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2023-24">Buy Here</a></sub></details>||
|Genesis|G70 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2018-19">Buy Here</a></sub></details>|| |Genesis|G70 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2018-19">Buy Here</a></sub></details>||
|Genesis|G70 2020-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2020-23">Buy Here</a></sub></details>|| |Genesis|G70 2020-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2020-21">Buy Here</a></sub></details>||
|Genesis|G70 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2022-23">Buy Here</a></sub></details>||
|Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai J connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G80 2017">Buy Here</a></sub></details>|| |Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai J connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G80 2017">Buy Here</a></sub></details>||
|Genesis|G80 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G80 2018-19">Buy Here</a></sub></details>|| |Genesis|G80 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G80 2018-19">Buy Here</a></sub></details>||
|Genesis|G90 2017-20|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G90 2017-20">Buy Here</a></sub></details>|| |Genesis|G90 2017-20|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G90 2017-20">Buy Here</a></sub></details>||
@ -71,7 +72,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Civic Hatchback 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Honda|Civic Hatchback 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Civic Hatchback 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Honda|CR-V 2015-16|Touring Trim|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2015-16">Buy Here</a></sub></details>|| |Honda|CR-V 2015-16|Touring Trim|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2015-16">Buy Here</a></sub></details>||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2017-22">Buy Here</a></sub></details>|| |Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V 2017-22">Buy Here</a></sub></details>||
|Honda|CR-V Hybrid 2017-20|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V Hybrid 2017-20">Buy Here</a></sub></details>|| |Honda|CR-V Hybrid 2017-21|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=CR-V Hybrid 2017-21">Buy Here</a></sub></details>||
|Honda|e 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=e 2020">Buy Here</a></sub></details>|| |Honda|e 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=e 2020">Buy Here</a></sub></details>||
|Honda|Fit 2018-20|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Fit 2018-20">Buy Here</a></sub></details>|| |Honda|Fit 2018-20|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Fit 2018-20">Buy Here</a></sub></details>||
|Honda|Freed 2020|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Freed 2020">Buy Here</a></sub></details>|| |Honda|Freed 2020|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Freed 2020">Buy Here</a></sub></details>||
@ -215,6 +216,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Škoda|Kodiaq 2017-23[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Škoda&model=Kodiaq 2017-23">Buy Here</a></sub></details>|| |Škoda|Kodiaq 2017-23[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Škoda&model=Kodiaq 2017-23">Buy Here</a></sub></details>||
|Škoda|Octavia 2015-19[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Škoda&model=Octavia 2015-19">Buy Here</a></sub></details>|| |Škoda|Octavia 2015-19[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Škoda&model=Octavia 2015-19">Buy Here</a></sub></details>||
|Škoda|Octavia RS 2016[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Škoda&model=Octavia RS 2016">Buy Here</a></sub></details>|| |Škoda|Octavia RS 2016[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Škoda&model=Octavia RS 2016">Buy Here</a></sub></details>||
|Škoda|Octavia Scout 2017-19[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Škoda&model=Octavia Scout 2017-19">Buy Here</a></sub></details>||
|Škoda|Scala 2020-23[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Škoda&model=Scala 2020-23">Buy Here</a></sub></details>[<sup>14</sup>](#footnotes)|| |Škoda|Scala 2020-23[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Škoda&model=Scala 2020-23">Buy Here</a></sub></details>[<sup>14</sup>](#footnotes)||
|Škoda|Superb 2015-22[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Škoda&model=Superb 2015-22">Buy Here</a></sub></details>|| |Škoda|Superb 2015-22[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Škoda&model=Superb 2015-22">Buy Here</a></sub></details>||
|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Alphard 2019-20">Buy Here</a></sub></details>|| |Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Alphard 2019-20">Buy Here</a></sub></details>||

@ -1 +1 @@
Subproject commit ff1f1ff335261c469635c57c81817afd04663eab Subproject commit 5821bd94d0cb9017d274b4499f2a0525ac317dc2

@ -1 +1 @@
Subproject commit 567dbfe6d86ddda6d803da371942603c6dbe36c8 Subproject commit 18f0bdff4bfb178c5cb5613fe91f0bb424791a93

674
poetry.lock generated

@ -730,19 +730,19 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"]
[[package]] [[package]]
name = "control" name = "control"
version = "0.9.4" version = "0.10.0"
description = "Python Control Systems Library" description = "Python Control Systems Library"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.10"
files = [ files = [
{file = "control-0.9.4-py3-none-any.whl", hash = "sha256:ab68980abd8d35ae5015ffa090865cbbd926deea7e66d0b9a41cfd12577e63ff"}, {file = "control-0.10.0-py3-none-any.whl", hash = "sha256:ed1e0eb73f1e2945fc9af9d7b9121ef328fe980e7903b2a14b149d4f1855a808"},
{file = "control-0.9.4.tar.gz", hash = "sha256:0fa57d2216b7ac4e9339c09eab6827660318a641779335864feee940bd19c9ce"}, {file = "control-0.10.0.tar.gz", hash = "sha256:2c18b767537f45c7fd07b2e4afe8fbe5964019499b5f52f888edb5d8560bab53"},
] ]
[package.dependencies] [package.dependencies]
matplotlib = "*" matplotlib = ">=3.6"
numpy = "*" numpy = ">=1.23"
scipy = ">=1.3" scipy = ">=1.8"
[package.extras] [package.extras]
cvxopt = ["cvxopt (>=1.2.0)"] cvxopt = ["cvxopt (>=1.2.0)"]
@ -894,69 +894,69 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"]
[[package]] [[package]]
name = "cython" name = "cython"
version = "3.0.9" version = "3.0.10"
description = "The Cython compiler for writing C extensions in the Python language." description = "The Cython compiler for writing C extensions in the Python language."
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
files = [ files = [
{file = "Cython-3.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:296bd30d4445ac61b66c9d766567f6e81a6e262835d261e903c60c891a6729d3"}, {file = "Cython-3.0.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e876272548d73583e90babda94c1299537006cad7a34e515a06c51b41f8657aa"},
{file = "Cython-3.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f496b52845cb45568a69d6359a2c335135233003e708ea02155c10ce3548aa89"}, {file = "Cython-3.0.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc377aa33c3309191e617bf675fdbb51ca727acb9dc1aa23fc698d8121f7e23"},
{file = "Cython-3.0.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:858c3766b9aa3ab8a413392c72bbab1c144a9766b7c7bfdef64e2e414363fa0c"}, {file = "Cython-3.0.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:401aba1869a57aba2922ccb656a6320447e55ace42709b504c2f8e8b166f46e1"},
{file = "Cython-3.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0eb1e6ef036028a52525fd9a012a556f6dd4788a0e8755fe864ba0e70cde2ff"}, {file = "Cython-3.0.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:541fbe725d6534a90b93f8c577eb70924d664b227a4631b90a6e0506d1469591"},
{file = "Cython-3.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c8191941073ea5896321de3c8c958fd66e5f304b0cd1f22c59edd0b86c4dd90d"}, {file = "Cython-3.0.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:86998b01f6a6d48398df8467292c7637e57f7e3a2ca68655367f13f66fed7734"},
{file = "Cython-3.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e32b016030bc72a8a22a1f21f470a2f57573761a4f00fbfe8347263f4fbdb9f1"}, {file = "Cython-3.0.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d092c0ddba7e9e530a5c5be4ac06db8360258acc27675d1fc86294a5dc8994c5"},
{file = "Cython-3.0.9-cp310-cp310-win32.whl", hash = "sha256:d6f3ff1cd6123973fe03e0fb8ee936622f976c0c41138969975824d08886572b"}, {file = "Cython-3.0.10-cp310-cp310-win32.whl", hash = "sha256:3cffb666e649dba23810732497442fb339ee67ba4e0be1f0579991e83fcc2436"},
{file = "Cython-3.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:56f3b643dbe14449248bbeb9a63fe3878a24256664bc8c8ef6efd45d102596d8"}, {file = "Cython-3.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:9ea31184c7b3a728ef1f81fccb161d8948c05aa86c79f63b74fb6f3ddec860ec"},
{file = "Cython-3.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:35e6665a20d6b8a152d72b7fd87dbb2af6bb6b18a235b71add68122d594dbd41"}, {file = "Cython-3.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:051069638abfb076900b0c2bcb6facf545655b3f429e80dd14365192074af5a4"},
{file = "Cython-3.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92f4960c40ad027bd8c364c50db11104eadc59ffeb9e5b7f605ca2f05946e20"}, {file = "Cython-3.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:712760879600907189c7d0d346851525545484e13cd8b787e94bfd293da8ccf0"},
{file = "Cython-3.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38df37d0e732fbd9a2fef898788492e82b770c33d1e4ed12444bbc8a3b3f89c0"}, {file = "Cython-3.0.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38d40fa1324ac47c04483d151f5e092406a147eac88a18aec789cf01c089c3f2"},
{file = "Cython-3.0.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad7fd88ebaeaf2e76fd729a8919fae80dab3d6ac0005e28494261d52ff347a8f"}, {file = "Cython-3.0.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bd49a3a9fdff65446a3e1c2bfc0ec85c6ce4c3cad27cd4ad7ba150a62b7fb59"},
{file = "Cython-3.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1365d5f76bf4d19df3d19ce932584c9bb76e9fb096185168918ef9b36e06bfa4"}, {file = "Cython-3.0.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e8df79b596633b8295eaa48b1157d796775c2bb078f32267d32f3001b687f2fd"},
{file = "Cython-3.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c232e7f279388ac9625c3e5a5a9f0078a9334959c5d6458052c65bbbba895e1e"}, {file = "Cython-3.0.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bcc9795990e525c192bc5c0775e441d7d56d7a7d02210451e9e13c0448dba51b"},
{file = "Cython-3.0.9-cp311-cp311-win32.whl", hash = "sha256:357e2fad46a25030b0c0496487e01a9dc0fdd0c09df0897f554d8ba3c1bc4872"}, {file = "Cython-3.0.10-cp311-cp311-win32.whl", hash = "sha256:09f2000041db482cad3bfce94e1fa3a4c82b0e57390a164c02566cbbda8c4f12"},
{file = "Cython-3.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:1315aee506506e8d69cf6631d8769e6b10131fdcc0eb66df2698f2a3ddaeeff2"}, {file = "Cython-3.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:3919a55ec9b6c7db6f68a004c21c05ed540c40dbe459ced5d801d5a1f326a053"},
{file = "Cython-3.0.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:157973807c2796addbed5fbc4d9c882ab34bbc60dc297ca729504901479d5df7"}, {file = "Cython-3.0.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8f2864ab5fcd27a346f0b50f901ebeb8f60b25a60a575ccfd982e7f3e9674914"},
{file = "Cython-3.0.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00b105b5d050645dd59e6767bc0f18b48a4aa11c85f42ec7dd8181606f4059e3"}, {file = "Cython-3.0.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:407840c56385b9c085826fe300213e0e76ba15d1d47daf4b58569078ecb94446"},
{file = "Cython-3.0.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac5536d09bef240cae0416d5a703d298b74c7bbc397da803ac9d344e732d4369"}, {file = "Cython-3.0.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a036d00caa73550a3a976432ef21c1e3fa12637e1616aab32caded35331ae96"},
{file = "Cython-3.0.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09c44501d476d16aaa4cbc29c87f8c0f54fc20e69b650d59cbfa4863426fc70c"}, {file = "Cython-3.0.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc6a0e7e23a96dec3f3c9d39690d4281beabd5297855140d0d30855f950275e"},
{file = "Cython-3.0.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:cc9c3b9f20d8e298618e5ccd32083ca386e785b08f9893fbec4c50b6b85be772"}, {file = "Cython-3.0.10-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5e14a8c6a8157d2b0cdc2e8e3444905d20a0e78e19d2a097e89fb8b04b51f6b"},
{file = "Cython-3.0.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a30d96938c633e3ec37000ac3796525da71254ef109e66bdfd78f29891af6454"}, {file = "Cython-3.0.10-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f8a2b8fa0fd8358bccb5f3304be563c4750aae175100463d212d5ea0ec74cbe0"},
{file = "Cython-3.0.9-cp312-cp312-win32.whl", hash = "sha256:757ca93bdd80702546df4d610d2494ef2e74249cac4d5ba9464589fb464bd8a3"}, {file = "Cython-3.0.10-cp312-cp312-win32.whl", hash = "sha256:2d29e617fd23cf4b83afe8f93f2966566c9f565918ad1e86a4502fe825cc0a79"},
{file = "Cython-3.0.9-cp312-cp312-win_amd64.whl", hash = "sha256:1dc320a9905ab95414013f6de805efbff9e17bb5fb3b90bbac533f017bec8136"}, {file = "Cython-3.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:6c5af936940a38c300977b81598d9c0901158f220a58c177820e17e1774f1cf1"},
{file = "Cython-3.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4ae349960ebe0da0d33724eaa7f1eb866688fe5434cc67ce4dbc06d6a719fbfc"}, {file = "Cython-3.0.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5f465443917d5c0f69825fca3b52b64c74ac3de0143b1fff6db8ba5b48c9fb4a"},
{file = "Cython-3.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63d2537bf688247f76ded6dee28ebd26274f019309aef1eb4f2f9c5c482fde2d"}, {file = "Cython-3.0.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fadb84193c25641973666e583df8df4e27c52cdc05ddce7c6f6510d690ba34a"},
{file = "Cython-3.0.9-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36f5a2dfc724bea1f710b649f02d802d80fc18320c8e6396684ba4a48412445a"}, {file = "Cython-3.0.10-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fa9e7786083b6aa61594c16979d621b62e61fcd9c2edd4761641b95c7fb34b2"},
{file = "Cython-3.0.9-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:deaf4197d4b0bcd5714a497158ea96a2bd6d0f9636095437448f7e06453cc83d"}, {file = "Cython-3.0.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4780d0f98ce28191c4d841c4358b5d5e79d96520650910cd59904123821c52d"},
{file = "Cython-3.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:000af6deb7412eb7ac0c635ff5e637fb8725dd0a7b88cc58dfc2b3de14e701c4"}, {file = "Cython-3.0.10-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:32fbad02d1189be75eb96456d9c73f5548078e5338d8fa153ecb0115b6ee279f"},
{file = "Cython-3.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:15c7f5c2d35bed9aa5f2a51eaac0df23ae72f2dbacf62fc672dd6bfaa75d2d6f"}, {file = "Cython-3.0.10-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:90e2f514fc753b55245351305a399463103ec18666150bb1c36779b9862388e9"},
{file = "Cython-3.0.9-cp36-cp36m-win32.whl", hash = "sha256:f49aa4970cd3bec66ac22e701def16dca2a49c59cceba519898dd7526e0be2c0"}, {file = "Cython-3.0.10-cp36-cp36m-win32.whl", hash = "sha256:a9c976e9ec429539a4367cb4b24d15a1e46b925976f4341143f49f5f161171f5"},
{file = "Cython-3.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:4558814fa025b193058d42eeee498a53d6b04b2980d01339fc2444b23fd98e58"}, {file = "Cython-3.0.10-cp36-cp36m-win_amd64.whl", hash = "sha256:a9bb402674788a7f4061aeef8057632ec440123e74ed0fb425308a59afdfa10e"},
{file = "Cython-3.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:539cd1d74fd61f6cfc310fa6bbbad5adc144627f2b7486a07075d4e002fd6aad"}, {file = "Cython-3.0.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:206e803598010ecc3813db8748ed685f7beeca6c413f982df9f8a505fce56563"},
{file = "Cython-3.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232926cd406ee02eabb732206f6e882c3aed9d58f0fea764013d9240405bcf"}, {file = "Cython-3.0.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15b6d397f4ee5ad54e373589522af37935a32863f1b23fa8c6922adf833e28e2"},
{file = "Cython-3.0.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33b6ac376538a7fc8c567b85d3c71504308a9318702ec0485dd66c059f3165cb"}, {file = "Cython-3.0.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a181144c2f893ed8e6a994d43d0b96300bc99873f21e3b7334ca26c61c37b680"},
{file = "Cython-3.0.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2cc92504b5d22ac66031ffb827bd3a967fc75a5f0f76ab48bce62df19be6fdfd"}, {file = "Cython-3.0.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74b700d6a793113d03fb54b63bdbadba6365379424bac7c0470605672769260"},
{file = "Cython-3.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:22b8fae756c5c0d8968691bed520876de452f216c28ec896a00739a12dba3bd9"}, {file = "Cython-3.0.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:076e9fd4e0ca33c5fa00a7479180dbfb62f17fe928e2909f82da814536e96d2b"},
{file = "Cython-3.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9cda0d92a09f3520f29bd91009f1194ba9600777c02c30c6d2d4ac65fb63e40d"}, {file = "Cython-3.0.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:269f06e6961e8591d56e30b46e1a51b6ccb42cab04c29fa3b30d3e8723485fb4"},
{file = "Cython-3.0.9-cp37-cp37m-win32.whl", hash = "sha256:ec612418490941ed16c50c8d3784c7bdc4c4b2a10c361259871790b02ec8c1db"}, {file = "Cython-3.0.10-cp37-cp37m-win32.whl", hash = "sha256:d4e83a8ceff7af60064da4ccfce0ac82372544dd5392f1b350c34f1b04d0fae6"},
{file = "Cython-3.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:976c8d2bedc91ff6493fc973d38b2dc01020324039e2af0e049704a8e1b22936"}, {file = "Cython-3.0.10-cp37-cp37m-win_amd64.whl", hash = "sha256:40fac59c3a7fbcd9c25aea64c342c890a5e2270ce64a1525e840807800167799"},
{file = "Cython-3.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5055988b007c92256b6e9896441c3055556038c3497fcbf8c921a6c1fce90719"}, {file = "Cython-3.0.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f43a58bf2434870d2fc42ac2e9ff8138c9e00c6251468de279d93fa279e9ba3b"},
{file = "Cython-3.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9360606d964c2d0492a866464efcf9d0a92715644eede3f6a2aa696de54a137"}, {file = "Cython-3.0.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e9a885ec63d3955a08cefc4eec39fefa9fe14989c6e5e2382bd4aeb6bdb9bc3"},
{file = "Cython-3.0.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c6e809f060bed073dc7cba1648077fe3b68208863d517c8b39f3920eecf9dd"}, {file = "Cython-3.0.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acfbe0fff364d54906058fc61f2393f38cd7fa07d344d80923937b87e339adcf"},
{file = "Cython-3.0.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95ed792c966f969cea7489c32ff90150b415c1f3567db8d5a9d489c7c1602dac"}, {file = "Cython-3.0.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8adcde00a8a88fab27509b558cd8c2959ab0c70c65d3814cfea8c68b83fa6dcd"},
{file = "Cython-3.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8edd59d22950b400b03ca78d27dc694d2836a92ef0cac4f64cb4b2ff902f7e25"}, {file = "Cython-3.0.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2c9c1e3e78909488f3b16fabae02308423fa6369ed96ab1e250807d344cfffd7"},
{file = "Cython-3.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4cf0ed273bf60e97922fcbbdd380c39693922a597760160b4b4355e6078ca188"}, {file = "Cython-3.0.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc6e0faf5b57523b073f0cdefadcaef3a51235d519a0594865925cadb3aeadf0"},
{file = "Cython-3.0.9-cp38-cp38-win32.whl", hash = "sha256:5eb9bd4ae12ebb2bc79a193d95aacf090fbd8d7013e11ed5412711650cb34934"}, {file = "Cython-3.0.10-cp38-cp38-win32.whl", hash = "sha256:35f6ede7c74024ed1982832ae61c9fad7cf60cc3f5b8c6a63bb34e38bc291936"},
{file = "Cython-3.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:44457279da56e0f829bb1fc5a5dc0836e5d498dbcf9b2324f32f7cc9d2ec6569"}, {file = "Cython-3.0.10-cp38-cp38-win_amd64.whl", hash = "sha256:950c0c7b770d2a7cec74fb6f5ccc321d0b51d151f48c075c0d0db635a60ba1b5"},
{file = "Cython-3.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4b419a1adc2af43f4660e2f6eaf1e4fac2dbac59490771eb8ac3d6063f22356"}, {file = "Cython-3.0.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:077b61ee789e48700e25d4a16daa4258b8e65167136e457174df400cf9b4feab"},
{file = "Cython-3.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f836192140f033b2319a0128936367c295c2b32e23df05b03b672a6015757ea"}, {file = "Cython-3.0.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f1f8bba9d8f37c0cffc934792b4ac7c42d0891077127c11deebe9fa0a0f7e4"},
{file = "Cython-3.0.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd198c1a7f8e9382904d622cc0efa3c184605881fd5262c64cbb7168c4c1ec5"}, {file = "Cython-3.0.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a15a8534ebfb9b58cb0b87c269c70984b6f9c88bfe65e4f635f0e3f07dfcd"},
{file = "Cython-3.0.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a274fe9ca5c53fafbcf5c8f262f8ad6896206a466f0eeb40aaf36a7951e957c0"}, {file = "Cython-3.0.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d10fc9aa82e5e53a0b7fd118f9771199cddac8feb4a6d8350b7d4109085aa775"},
{file = "Cython-3.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:158c38360bbc5063341b1e78d3737f1251050f89f58a3df0d10fb171c44262be"}, {file = "Cython-3.0.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f610964ab252a83e573a427e28b103e2f1dd3c23bee54f32319f9e73c3c5499"},
{file = "Cython-3.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8bf30b045f7deda0014b042c1b41c1d272facc762ab657529e3b05505888e878"}, {file = "Cython-3.0.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8c9c4c4f3ab8f8c02817b0e16e8fa7b8cc880f76e9b63fe9c010e60c1a6c2b13"},
{file = "Cython-3.0.9-cp39-cp39-win32.whl", hash = "sha256:9a001fd95c140c94d934078544ff60a3c46aca2dc86e75a76e4121d3cd1f4b33"}, {file = "Cython-3.0.10-cp39-cp39-win32.whl", hash = "sha256:0bac3ccdd4e03924028220c62ae3529e17efa8ca7e9df9330de95de02f582b26"},
{file = "Cython-3.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:530c01c4aebba709c0ec9c7ecefe07177d0b9fd7ffee29450a118d92192ccbdf"}, {file = "Cython-3.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:81f356c1c8c0885b8435bfc468025f545c5d764aa9c75ab662616dd1193c331e"},
{file = "Cython-3.0.9-py2.py3-none-any.whl", hash = "sha256:bf96417714353c5454c2e3238fca9338599330cf51625cdc1ca698684465646f"}, {file = "Cython-3.0.10-py2.py3-none-any.whl", hash = "sha256:fcbb679c0b43514d591577fd0d20021c55c240ca9ccafbdb82d3fb95e5edfee2"},
{file = "Cython-3.0.9.tar.gz", hash = "sha256:a2d354f059d1f055d34cfaa62c5b68bc78ac2ceab6407148d47fb508cf3ba4f3"}, {file = "Cython-3.0.10.tar.gz", hash = "sha256:dcc96739331fb854dcf503f94607576cfe8488066c61ca50dfd55836f132de99"},
] ]
[[package]] [[package]]
@ -1045,18 +1045,18 @@ files = [
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.13.1" version = "3.13.3"
description = "A platform independent file lock." description = "A platform independent file lock."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, {file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"},
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, {file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"},
] ]
[package.extras] [package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
typing = ["typing-extensions (>=4.8)"] typing = ["typing-extensions (>=4.8)"]
[[package]] [[package]]
@ -1119,13 +1119,13 @@ files = [
[[package]] [[package]]
name = "flatbuffers" name = "flatbuffers"
version = "24.3.7" version = "24.3.25"
description = "The FlatBuffers serialization format for Python" description = "The FlatBuffers serialization format for Python"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "flatbuffers-24.3.7-py2.py3-none-any.whl", hash = "sha256:80c4f5dcad0ee76b7e349671a0d657f2fbba927a0244f88dd3f5ed6a3694e1fc"}, {file = "flatbuffers-24.3.25-py2.py3-none-any.whl", hash = "sha256:8dbdec58f935f3765e4f7f3cf635ac3a77f83568138d6a2311f524ec96364812"},
{file = "flatbuffers-24.3.7.tar.gz", hash = "sha256:0895c22b9a6019ff2f4de2e5e2f7cd15914043e6e7033a94c0c6369422690f22"}, {file = "flatbuffers-24.3.25.tar.gz", hash = "sha256:de2ec5b203f21441716617f38443e0a8ebf3d25bf0d9c0bb0ce68fa00ad546a4"},
] ]
[[package]] [[package]]
@ -1611,13 +1611,13 @@ files = [
[[package]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "7.0.2" version = "7.1.0"
description = "Read metadata from Python packages" description = "Read metadata from Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "importlib_metadata-7.0.2-py3-none-any.whl", hash = "sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100"}, {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"},
{file = "importlib_metadata-7.0.2.tar.gz", hash = "sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792"}, {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"},
] ]
[package.dependencies] [package.dependencies]
@ -1626,7 +1626,7 @@ zipp = ">=0.5"
[package.extras] [package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"] perf = ["ipython"]
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
@ -1931,96 +1931,132 @@ test = ["pytest"]
[[package]] [[package]]
name = "lxml" name = "lxml"
version = "5.1.0" version = "5.2.0"
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, {file = "lxml-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c54f8d6160080831a76780d850302fdeb0e8d0806f661777b0714dfb55d9a08a"},
{file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, {file = "lxml-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e95ae029396382a0d2e8174e4077f96befcd4a2184678db363ddc074eb4d3b2"},
{file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, {file = "lxml-5.2.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5810fa80e64a0c689262a71af999c5735f48c0da0affcbc9041d1ef5ef3920be"},
{file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, {file = "lxml-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae69524fd6a68b288574013f8fadac23cacf089c75cd3fc5b216277a445eb736"},
{file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"}, {file = "lxml-5.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fadda215e32fe375d65e560b7f7e2a37c7f9c4ecee5315bb1225ca6ac9bf5838"},
{file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"}, {file = "lxml-5.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:f1f164e4cc6bc646b1fc86664c3543bf4a941d45235797279b120dc740ee7af5"},
{file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"}, {file = "lxml-5.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3603a8a41097daf7672cae22cc4a860ab9ea5597f1c5371cb21beca3398b8d6a"},
{file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, {file = "lxml-5.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3b4bb89a785f4fd60e05f3c3a526c07d0d68e3536f17f169ca13bf5b5dd75a5"},
{file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, {file = "lxml-5.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1effc10bf782f0696e76ecfeba0720ea02c0c31d5bffb7b29ba10debd57d1c3d"},
{file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, {file = "lxml-5.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b03531f6cd6ce4b511dcece060ca20aa5412f8db449274b44f4003f282e6272f"},
{file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, {file = "lxml-5.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fac15090bb966719df06f0c4f8139783746d1e60e71016d8a65db2031ca41b8"},
{file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, {file = "lxml-5.2.0-cp310-cp310-win32.whl", hash = "sha256:92bb37c96215c4b2eb26f3c791c0bf02c64dd251effa532b43ca5049000c4478"},
{file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, {file = "lxml-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:b0181c22fdb89cc19e70240a850e5480817c3e815b1eceb171b3d7a3aa3e596a"},
{file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, {file = "lxml-5.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ada8ce9e6e1d126ef60d215baaa0c81381ba5841c25f1d00a71cdafdc038bd27"},
{file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"}, {file = "lxml-5.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cefb133c859f06dab2ae63885d9f405000c4031ec516e0ed4f9d779f690d8e3"},
{file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"}, {file = "lxml-5.2.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ede2a7a86a977b0c741654efaeca0af7860a9b1ae39f9268f0936246a977ee0"},
{file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"}, {file = "lxml-5.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d46df6f0b1a0cda39d12c5c4615a7d92f40342deb8001c7b434d7c8c78352e58"},
{file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, {file = "lxml-5.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2259243ee734cc736e237719037efb86603c891fd363cc7973a2d0ac8a0e3f"},
{file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, {file = "lxml-5.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c53164f29ed3c3868787144e8ea8a399ffd7d8215f59500a20173593c19e96eb"},
{file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, {file = "lxml-5.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:371aab9a397dcc76625ad3b02fa9b21be63406d69237b773156e7d1fc2ce0cae"},
{file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, {file = "lxml-5.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e08784288a179b59115b5e57abf6d387528b39abb61105fe17510a199a277a40"},
{file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, {file = "lxml-5.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c232726f7b6df5143415a06323faaa998ef8abbe1c0ed00d718755231d76f08"},
{file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, {file = "lxml-5.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e4366e58c0508da4dee4c7c70cee657e38553d73abdffa53abbd7d743711ee11"},
{file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, {file = "lxml-5.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c84dce8fb2e900d4fb094e76fdad34a5fd06de53e41bddc1502c146eb11abd74"},
{file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"}, {file = "lxml-5.2.0-cp311-cp311-win32.whl", hash = "sha256:0947d1114e337dc2aae2fa14bbc9ed5d9ca1a0acd6d2f948df9926aef65305e9"},
{file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"}, {file = "lxml-5.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1eace37a9f4a1bef0bb5c849434933fd6213008ec583c8e31ee5b8e99c7c8500"},
{file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"}, {file = "lxml-5.2.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f2cb157e279d28c66b1c27e0948687dc31dc47d1ab10ce0cd292a8334b7de3d5"},
{file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"}, {file = "lxml-5.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:53c0e56f41ef68c1ce4e96f27ecdc2df389730391a2fd45439eb3facb02d36c8"},
{file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"}, {file = "lxml-5.2.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703d60e59ab45c17485c2c14b11880e4f7f0eab07134afa9007573fa5a779a5a"},
{file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"}, {file = "lxml-5.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaf5e308a5e50bc0548c4fdca0117a31ec9596f8cfc96592db170bcecc71a957"},
{file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"}, {file = "lxml-5.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af64df85fecd3cf3b2e792f0b5b4d92740905adfa8ce3b24977a55415f1a0c40"},
{file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"}, {file = "lxml-5.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:df7dfbdef11702fd22c2eaf042d7098d17edbc62d73f2199386ad06cbe466f6d"},
{file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"}, {file = "lxml-5.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7250030a7835bfd5ba6ca7d1ad483ec90f9cbc29978c5e75c1cc3e031d3c4160"},
{file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"}, {file = "lxml-5.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:be5faa2d5c8c8294d770cfd09d119fb27b5589acc59635b0cf90f145dbe81dca"},
{file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"}, {file = "lxml-5.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:347ec08250d5950f5b016caa3e2e13fb2cb9714fe6041d52e3716fb33c208663"},
{file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"}, {file = "lxml-5.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc7b630c4fb428b8a40ddd0bfc4bc19de11bb3c9b031154f77360e48fe8b4451"},
{file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"}, {file = "lxml-5.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ae550cbd7f229cdf2841d9b01406bcca379a5fb327b9efb53ba620a10452e835"},
{file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"}, {file = "lxml-5.2.0-cp312-cp312-win32.whl", hash = "sha256:7c61ce3cdd6e6c9f4003ac118be7eb3036d0ce2afdf23929e533e54482780f74"},
{file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"}, {file = "lxml-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f90c36ca95a44d2636bbf55a51ca30583b59b71b6547b88d954e029598043551"},
{file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"}, {file = "lxml-5.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1cce2eaad7e38b985b0f91f18468dda0d6b91862d32bec945b0e46e2ffe7222e"},
{file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"}, {file = "lxml-5.2.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60a3983d32f722a8422c01e4dc4badc7a307ca55c59e2485d0e14244a52c482f"},
{file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"}, {file = "lxml-5.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60847dfbdfddf08a56c4eefe48234e8c1ab756c7eda4a2a7c1042666a5516564"},
{file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"}, {file = "lxml-5.2.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bbe335f0d1a86391671d975a1b5e9b08bb72fba6b567c43bdc2e55ca6e6c086"},
{file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, {file = "lxml-5.2.0-cp36-cp36m-manylinux_2_28_aarch64.whl", hash = "sha256:3ac7c8a60b8ad51fe7bca99a634dd625d66492c502fd548dc6dc769ce7d94b6a"},
{file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, {file = "lxml-5.2.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:73e69762cf740ac3ae81137ef9d6f15f93095f50854e233d50b29e7b8a91dbc6"},
{file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, {file = "lxml-5.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:281ee1ffeb0ab06204dfcd22a90e9003f0bb2dab04101ad983d0b1773bc10588"},
{file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, {file = "lxml-5.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ba3a86b0d5a5c93104cb899dff291e3ae13729c389725a876d00ef9696de5425"},
{file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, {file = "lxml-5.2.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:356f8873b1e27b81793e30144229adf70f6d3e36e5cb7b6d289da690f4398953"},
{file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, {file = "lxml-5.2.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:2a34e74ffe92c413f197ff4967fb1611d938ee0691b762d062ef0f73814f3aa4"},
{file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, {file = "lxml-5.2.0-cp36-cp36m-win32.whl", hash = "sha256:6f0d2b97a5a06c00c963d4542793f3e486b1ed3a957f8c19f6006ed39d104bb0"},
{file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, {file = "lxml-5.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:35e39c6fd089ad6674eb52d93aa874d6027b3ae44d2381cca6e9e4c2e102c9c8"},
{file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"}, {file = "lxml-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5f6e4e5a62114ae76690c4a04c5108d067442d0a41fd092e8abd25af1288c450"},
{file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, {file = "lxml-5.2.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93eede9bcc842f891b2267c7f0984d811940d1bc18472898a1187fe560907a99"},
{file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, {file = "lxml-5.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad364026c2cebacd7e01d1138bd53639822fefa8f7da90fc38cd0e6319a2699"},
{file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, {file = "lxml-5.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f06e4460e76468d99cc36d5b9bc6fc5f43e6662af44960e13e3f4e040aacb35"},
{file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, {file = "lxml-5.2.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ca3236f31d565555139d5b00b790ed2a98ac6f0c4470c4032f8b5e5a5dba3c1a"},
{file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, {file = "lxml-5.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:a9b67b850ab1d304cb706cf71814b0e0c3875287083d7ec55ee69504a9c48180"},
{file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, {file = "lxml-5.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5261c858c390ae9a19aba96796948b6a2d56649cbd572968970dc8da2b2b2a42"},
{file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, {file = "lxml-5.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e8359fb610c8c444ac473cfd82dae465f405ff807cabb98a9b9712bbd0028751"},
{file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"}, {file = "lxml-5.2.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:f9e27841cddfaebc4e3ffbe5dbdff42891051acf5befc9f5323944b2c61cef16"},
{file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"}, {file = "lxml-5.2.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:641a8da145aca67671205f3e89bfec9815138cf2fe06653c909eab42e486d373"},
{file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"}, {file = "lxml-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:931a3a13e0f574abce8f3152b207938a54304ccf7a6fd7dff1fdb2f6691d08af"},
{file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"}, {file = "lxml-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:246c93e2503c710cf02c7e9869dc0258223cbefe5e8f9ecded0ac0aa07fd2bf8"},
{file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"}, {file = "lxml-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:11acfcdf5a38cf89c48662123a5d02ae0a7d99142c7ee14ad90de5c96a9b6f06"},
{file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"}, {file = "lxml-5.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:200f70b5d95fc79eb9ed7f8c4888eef4e274b9bf380b829d3d52e9ed962e9231"},
{file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"}, {file = "lxml-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba4d02aed47c25be6775a40d55c5774327fdedba79871b7c2485e80e45750cb2"},
{file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"}, {file = "lxml-5.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e283b24c14361fe9e04026a1d06c924450415491b83089951d469509900d9f32"},
{file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"}, {file = "lxml-5.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:03e3962d6ad13a862dacd5b3a3ea60b4d092a550f36465234b8639311fd60989"},
{file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"}, {file = "lxml-5.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6e45fd5213e5587a610b7e7c8c5319a77591ab21ead42df46bb342e21bc1418d"},
{file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"}, {file = "lxml-5.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:27877732946843f4b6bfc56eb40d865653eef34ad2edeed16b015d5c29c248df"},
{file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"}, {file = "lxml-5.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4d16b44ad0dd8c948129639e34c8d301ad87ebc852568ace6fe9a5ad9ce67ee1"},
{file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"}, {file = "lxml-5.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b8f842df9ba26135c5414e93214e04fe0af259bb4f96a32f756f89467f7f3b45"},
{file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"}, {file = "lxml-5.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c74e77df9e36c8c91157853e6cd400f6f9ca7a803ba89981bfe3f3fc7e5651ef"},
{file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"}, {file = "lxml-5.2.0-cp38-cp38-win32.whl", hash = "sha256:1459a998c10a99711ac532abe5cc24ba354e4396dafef741c7797f8830712d56"},
{file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"}, {file = "lxml-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:a00f5931b7cccea775123c3c0a2513aee58afdad8728550cc970bff32280bdd2"},
{file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"}, {file = "lxml-5.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ddda5ba8831f258ac7e6364be03cb27aa62f50c67fd94bc1c3b6247959cc0369"},
{file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"}, {file = "lxml-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56835b9e9a7767202fae06310c6b67478963e535fe185bed3bf9af5b18d2b67e"},
{file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"}, {file = "lxml-5.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25fef8794f0dc89f01bdd02df6a7fec4bcb2fbbe661d571e898167a83480185e"},
{file = "lxml-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32d44af078485c4da9a7ec460162392d49d996caf89516fa0b75ad0838047122"},
{file = "lxml-5.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f354d62345acdf22aa3e171bd9723790324a66fafe61bfe3873b86724cf6daaa"},
{file = "lxml-5.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6a7e0935f05e1cf1a3aa1d49a87505773b04f128660eac2a24a5594ea6b1baa7"},
{file = "lxml-5.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:75a4117b43694c72a0d89f6c18a28dc57407bde4650927d4ef5fd384bdf6dcc7"},
{file = "lxml-5.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:57402d6cdd8a897ce21cf8d1ff36683583c17a16322a321184766c89a1980600"},
{file = "lxml-5.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:56591e477bea531e5e1854f5dfb59309d5708669bc921562a35fd9ca5182bdcd"},
{file = "lxml-5.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7efbce96719aa275d49ad5357886845561328bf07e1d5ab998f4e3066c5ccf15"},
{file = "lxml-5.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a3c39def0965e8fb5c8d50973e0c7b4ce429a2fa730f3f9068a7f4f9ce78410b"},
{file = "lxml-5.2.0-cp39-cp39-win32.whl", hash = "sha256:5188f22c00381cb44283ecb28c8d85c2db4a3035774dd851876c8647cb809c27"},
{file = "lxml-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:ed1fe80e1fcdd1205a443bddb1ad3c3135bb1cd3f36cc996a1f4aed35960fbe8"},
{file = "lxml-5.2.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d2b339fb790fc923ae2e9345c8633e3d0064d37ea7920c027f20c8ae6f65a91f"},
{file = "lxml-5.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06036d60fccb21e22dd167f6d0e422b9cbdf3588a7e999a33799f9cbf01e41a5"},
{file = "lxml-5.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1611fb9de0a269c05575c024e6d8cdf2186e3fa52b364e3b03dcad82514d57"},
{file = "lxml-5.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:05fc3720250d221792b6e0d150afc92d20cb10c9cdaa8c8f93c2a00fbdd16015"},
{file = "lxml-5.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:11e41ffd3cd27b0ca1c76073b27bd860f96431d9b70f383990f1827ca19f2f52"},
{file = "lxml-5.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0382e6a3eefa3f6699b14fa77c2eb32af2ada261b75120eaf4fc028a20394975"},
{file = "lxml-5.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:be5c8e776ecbcf8c1bce71a7d90e3a3680c9ceae516cac0be08b47e9fac0ca43"},
{file = "lxml-5.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da12b4efc93d53068888cb3b58e355b31839f2428b8f13654bd25d68b201c240"},
{file = "lxml-5.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f46f8033da364bacc74aca5e319509a20bb711c8a133680ca5f35020f9eaf025"},
{file = "lxml-5.2.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:50a26f68d090594477df8572babac64575cd5c07373f7a8319c527c8e56c0f99"},
{file = "lxml-5.2.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:57cbadf028727705086047994d2e50124650e63ce5a035b0aa79ab50f001989f"},
{file = "lxml-5.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8aa11638902ac23f944f16ce45c9f04c9d5d57bb2da66822abb721f4efe5fdbb"},
{file = "lxml-5.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7150e630b879390e02121e71ceb1807f682b88342e2ea2082e2c8716cf8bd93"},
{file = "lxml-5.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4add722393c99da4d51c8d9f3e1ddf435b30677f2d9ba9aeaa656f23c1b7b580"},
{file = "lxml-5.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd0f25a431cd16f70ec1c47c10b413e7ddfe1ccaaddd1a7abd181e507c012374"},
{file = "lxml-5.2.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:883e382695f346c2ea3ad96bdbdf4ca531788fbeedb4352be3a8fcd169fc387d"},
{file = "lxml-5.2.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:80cc2b55bb6e35d3cb40936b658837eb131e9f16357241cd9ba106ae1e9c5ecb"},
{file = "lxml-5.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:59ec2948385336e9901008fdf765780fe30f03e7fdba8090aafdbe5d1b7ea0cd"},
{file = "lxml-5.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ddbea6e58cce1a640d9d65947f1e259423fc201c9cf9761782f355f53b7f3097"},
{file = "lxml-5.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52d6cdea438eb7282c41c5ac00bd6d47d14bebb6e8a8d2a1c168ed9e0cacfbab"},
{file = "lxml-5.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c556bbf88a8b667c849d326dd4dd9c6290ede5a33383ffc12b0ed17777f909d"},
{file = "lxml-5.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:947fa8bf15d1c62c6db36c6ede9389cac54f59af27010251747f05bddc227745"},
{file = "lxml-5.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e6cb8f7a332eaa2d876b649a748a445a38522e12f2168e5e838d1505a91cdbb7"},
{file = "lxml-5.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:16e65223f34fd3d65259b174f0f75a4bb3d9893698e5e7d01e54cd8c5eb98d85"},
{file = "lxml-5.2.0.tar.gz", hash = "sha256:21dc490cdb33047bc7f7ad76384f3366fa8f5146b86cc04c4af45de901393b90"},
] ]
[package.extras] [package.extras]
cssselect = ["cssselect (>=0.7)"] cssselect = ["cssselect (>=0.7)"]
html-clean = ["lxml-html-clean"]
html5 = ["html5lib"] html5 = ["html5lib"]
htmlsoup = ["BeautifulSoup4"] htmlsoup = ["BeautifulSoup4"]
source = ["Cython (>=3.0.7)"] source = ["Cython (>=3.0.10)"]
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
@ -2282,13 +2318,13 @@ tests = ["pytest (>=4.6)"]
[[package]] [[package]]
name = "msal" name = "msal"
version = "1.27.0" version = "1.28.0"
description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect."
optional = false optional = false
python-versions = ">=2.7" python-versions = ">=3.7"
files = [ files = [
{file = "msal-1.27.0-py2.py3-none-any.whl", hash = "sha256:572d07149b83e7343a85a3bcef8e581167b4ac76befcbbb6eef0c0e19643cdc0"}, {file = "msal-1.28.0-py3-none-any.whl", hash = "sha256:3064f80221a21cd535ad8c3fafbb3a3582cd9c7e9af0bb789ae14f726a0ca99b"},
{file = "msal-1.27.0.tar.gz", hash = "sha256:3109503c038ba6b307152b0e8d34f98113f2e7a78986e28d0baf5b5303afda52"}, {file = "msal-1.28.0.tar.gz", hash = "sha256:80bbabe34567cb734efd2ec1869b2d98195c927455369d8077b3c542088c5c9d"},
] ]
[package.dependencies] [package.dependencies]
@ -2568,40 +2604,46 @@ files = [
[[package]] [[package]]
name = "onnx" name = "onnx"
version = "1.15.0" version = "1.16.0"
description = "Open Neural Network Exchange" description = "Open Neural Network Exchange"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "onnx-1.15.0-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:51cacb6aafba308aaf462252ced562111f6991cdc7bc57a6c554c3519453a8ff"}, {file = "onnx-1.16.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:9eadbdce25b19d6216f426d6d99b8bc877a65ed92cbef9707751c6669190ba4f"},
{file = "onnx-1.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:0aee26b6f7f7da7e840de75ad9195a77a147d0662c94eaa6483be13ba468ffc1"}, {file = "onnx-1.16.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:034ae21a2aaa2e9c14119a840d2926d213c27aad29e5e3edaa30145a745048e1"},
{file = "onnx-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baf6ef6c93b3b843edb97a8d5b3d229a1301984f3f8dee859c29634d2083e6f9"}, {file = "onnx-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec22a43d74eb1f2303373e2fbe7fbcaa45fb225f4eb146edfed1356ada7a9aea"},
{file = "onnx-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ed899fe6000edc05bb2828863d3841cfddd5a7cf04c1a771f112e94de75d9f"}, {file = "onnx-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:298f28a2b5ac09145fa958513d3d1e6b349ccf86a877dbdcccad57713fe360b3"},
{file = "onnx-1.15.0-cp310-cp310-win32.whl", hash = "sha256:f1ad3d77fc2f4b4296f0ac2c8cadd8c1dcf765fc586b737462d3a0fe8f7c696a"}, {file = "onnx-1.16.0-cp310-cp310-win32.whl", hash = "sha256:66300197b52beca08bc6262d43c103289c5d45fde43fb51922ed1eb83658cf0c"},
{file = "onnx-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:ca4ebc4f47109bfb12c8c9e83dd99ec5c9f07d2e5f05976356c6ccdce3552010"}, {file = "onnx-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae0029f5e47bf70a1a62e7f88c80bca4ef39b844a89910039184221775df5e43"},
{file = "onnx-1.15.0-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:233ffdb5ca8cc2d960b10965a763910c0830b64b450376da59207f454701f343"}, {file = "onnx-1.16.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:f51179d4af3372b4f3800c558d204b592c61e4b4a18b8f61e0eea7f46211221a"},
{file = "onnx-1.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:51fa79c9ea9af033638ec51f9177b8e76c55fad65bb83ea96ee88fafade18ee7"}, {file = "onnx-1.16.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5202559070afec5144332db216c20f2fff8323cf7f6512b0ca11b215eacc5bf3"},
{file = "onnx-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f277d4861729f5253a51fa41ce91bfec1c4574ee41b5637056b43500917295ce"}, {file = "onnx-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77579e7c15b4df39d29465b216639a5f9b74026bdd9e4b6306cd19a32dcfe67c"},
{file = "onnx-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8a7c94d2ebead8f739fdb70d1ce5a71726f4e17b3e5b8ad64455ea1b2801a85"}, {file = "onnx-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e60ca76ac24b65c25860d0f2d2cdd96d6320d062a01dd8ce87c5743603789b8"},
{file = "onnx-1.15.0-cp311-cp311-win32.whl", hash = "sha256:17dcfb86a8c6bdc3971443c29b023dd9c90ff1d15d8baecee0747a6b7f74e650"}, {file = "onnx-1.16.0-cp311-cp311-win32.whl", hash = "sha256:81b4ee01bc554e8a2b11ac6439882508a5377a1c6b452acd69a1eebb83571117"},
{file = "onnx-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:60a3e28747e305cd2e766e6a53a0a6d952cf9e72005ec6023ce5e07666676a4e"}, {file = "onnx-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:7449241e70b847b9c3eb8dae622df8c1b456d11032a9d7e26e0ee8a698d5bf86"},
{file = "onnx-1.15.0-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:6b5c798d9e0907eaf319e3d3e7c89a2ed9a854bcb83da5fefb6d4c12d5e90721"}, {file = "onnx-1.16.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:03a627488b1a9975d95d6a55582af3e14c7f3bb87444725b999935ddd271d352"},
{file = "onnx-1.15.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:a4f774ff50092fe19bd8f46b2c9b27b1d30fbd700c22abde48a478142d464322"}, {file = "onnx-1.16.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c392faeabd9283ee344ccb4b067d1fea9dfc614fa1f0de7c47589efd79e15e78"},
{file = "onnx-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2b0e7f3938f2d994c34616bfb8b4b1cebbc4a0398483344fe5e9f2fe95175e6"}, {file = "onnx-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0efeb46985de08f0efe758cb54ad3457e821a05c2eaf5ba2ccb8cd1602c08084"},
{file = "onnx-1.15.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49cebebd0020a4b12c1dd0909d426631212ef28606d7e4d49463d36abe7639ad"}, {file = "onnx-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddf14a3d32234f23e44abb73a755cb96a423fac7f004e8f046f36b10214151ee"},
{file = "onnx-1.15.0-cp38-cp38-win32.whl", hash = "sha256:1fdf8a3ff75abc2b32c83bf27fb7c18d6b976c9c537263fadd82b9560fe186fa"}, {file = "onnx-1.16.0-cp312-cp312-win32.whl", hash = "sha256:62a2e27ae8ba5fc9b4a2620301446a517b5ffaaf8566611de7a7c2160f5bcf4c"},
{file = "onnx-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:763e55c26e8de3a2dce008d55ae81b27fa8fb4acbb01a29b9f3c01f200c4d676"}, {file = "onnx-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:3e0860fea94efde777e81a6f68f65761ed5e5f3adea2e050d7fbe373a9ae05b3"},
{file = "onnx-1.15.0-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:b2d5e802837629fc9c86f19448d19dd04d206578328bce202aeb3d4bedab43c4"}, {file = "onnx-1.16.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:70a90649318f3470985439ea078277c9fb2a2e6e2fd7c8f3f2b279402ad6c7e6"},
{file = "onnx-1.15.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:9a9cfbb5e5d5d88f89d0dfc9df5fb858899db874e1d5ed21e76c481f3cafc90d"}, {file = "onnx-1.16.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:71839546b7f93be4fa807995b182ab4b4414c9dbf049fee11eaaced16fcf8df2"},
{file = "onnx-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f472bbe5cb670a0a4a4db08f41fde69b187a009d0cb628f964840d3f83524e9"}, {file = "onnx-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7665217c45a61eb44718c8e9349d2ad004efa0cb9fbc4be5c6d5e18b9fe12b52"},
{file = "onnx-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf2de9bef64792e5b8080c678023ac7d2b9e05d79a3e17e92cf6a4a624831d2"}, {file = "onnx-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5752bbbd5717304a7643643dba383a2fb31e8eb0682f4e7b7d141206328a73b"},
{file = "onnx-1.15.0-cp39-cp39-win32.whl", hash = "sha256:ef4d9eb44b111e69e4534f3233fc2c13d1e26920d24ae4359d513bd54694bc6d"}, {file = "onnx-1.16.0-cp38-cp38-win32.whl", hash = "sha256:257858cbcb2055284f09fa2ae2b1cfd64f5850367da388d6e7e7b05920a40c90"},
{file = "onnx-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:95d7a3e2d79d371e272e39ae3f7547e0b116d0c7f774a4004e97febe6c93507f"}, {file = "onnx-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:209fe84995a28038e29ae8369edd35f33e0ef1ebc3bddbf6584629823469deb1"},
{file = "onnx-1.15.0.tar.gz", hash = "sha256:b18461a7d38f286618ca2a6e78062a2a9c634ce498e631e708a8041b00094825"}, {file = "onnx-1.16.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:8cf3e518b1b1b960be542e7c62bed4e5219e04c85d540817b7027029537dec92"},
] {file = "onnx-1.16.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:30f02beaf081c7d9fa3a8c566a912fc4408e28fc33b1452d58f890851691d364"},
{file = "onnx-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fb29a9a692b522deef1f6b8f2145da62c0c43ea1ed5b4c0f66f827fdc28847d"},
[package.dependencies] {file = "onnx-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7755cbd5f4e47952e37276ea5978a46fc8346684392315902b5ed4a719d87d06"},
numpy = "*" {file = "onnx-1.16.0-cp39-cp39-win32.whl", hash = "sha256:7532343dc5b8b5e7c3e3efa441a3100552f7600155c4db9120acd7574f64ffbf"},
{file = "onnx-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:d7886c05aa6d583ec42f6287678923c1e343afc4350e49d5b36a0023772ffa22"},
{file = "onnx-1.16.0.tar.gz", hash = "sha256:237c6987c6c59d9f44b6136f5819af79574f8d96a760a1fa843bede11f3822f7"},
]
[package.dependencies]
numpy = ">=1.20"
protobuf = ">=3.20.2" protobuf = ">=3.20.2"
[package.extras] [package.extras]
@ -2827,13 +2869,13 @@ panda3d-simplepbr = ">=0.6"
[[package]] [[package]]
name = "panda3d-simplepbr" name = "panda3d-simplepbr"
version = "0.11.2" version = "0.12.0"
description = "A simple, lightweight PBR render pipeline for Panda3D" description = "A straight-forward, easy-to-use, drop-in, PBR replacement for Panda3D's builtin auto shader"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "panda3d-simplepbr-0.11.2.tar.gz", hash = "sha256:89469f9be0e15fd8888a277f60ba08eb31b90354dcba80fc5aa7ccccee56be2a"}, {file = "panda3d-simplepbr-0.12.0.tar.gz", hash = "sha256:c71d490afeeb3a90455dcfde1d30c41f321a38742a97d18834e5c31016331ed5"},
{file = "panda3d_simplepbr-0.11.2-py3-none-any.whl", hash = "sha256:eca198004ed4a6635ac9180695440390d52c73eec0b6e02c317d000e1e4d1518"}, {file = "panda3d_simplepbr-0.12.0-py3-none-any.whl", hash = "sha256:6c43d1990ff07840cf1c557561d6122fd1250d8e76aacf227b61c3789149bcf9"},
] ]
[package.dependencies] [package.dependencies]
@ -2928,79 +2970,80 @@ dev = ["jinja2"]
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "10.2.0" version = "10.3.0"
description = "Python Imaging Library (Fork)" description = "Python Imaging Library (Fork)"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
{file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
{file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
{file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
{file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
{file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
{file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
{file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
{file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
{file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
{file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
{file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
{file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
{file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
{file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
{file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
{file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
{file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
{file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
{file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
{file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
{file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
{file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
{file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
{file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
{file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
{file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
] ]
[package.extras] [package.extras]
@ -3087,13 +3130,13 @@ files = [
[[package]] [[package]]
name = "pre-commit" name = "pre-commit"
version = "3.6.2" version = "3.7.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks." description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"},
{file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"},
] ]
[package.dependencies] [package.dependencies]
@ -3115,22 +3158,22 @@ files = [
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "5.26.0" version = "5.26.1"
description = "" description = ""
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "protobuf-5.26.0-cp310-abi3-win32.whl", hash = "sha256:f9ecc8eb6f18037e0cbf43256db0325d4723f429bca7ef5cd358b7c29d65f628"}, {file = "protobuf-5.26.1-cp310-abi3-win32.whl", hash = "sha256:3c388ea6ddfe735f8cf69e3f7dc7611e73107b60bdfcf5d0f024c3ccd3794e23"},
{file = "protobuf-5.26.0-cp310-abi3-win_amd64.whl", hash = "sha256:dfd29f6eb34107dccf289a93d44fb6b131e68888d090b784b691775ac84e8213"}, {file = "protobuf-5.26.1-cp310-abi3-win_amd64.whl", hash = "sha256:e6039957449cb918f331d32ffafa8eb9255769c96aa0560d9a5bf0b4e00a2a33"},
{file = "protobuf-5.26.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:7e47c57303466c867374a17b2b5e99c5a7c8b72a94118e2f28efb599f19b4069"}, {file = "protobuf-5.26.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:38aa5f535721d5bb99861166c445c4105c4e285c765fbb2ac10f116e32dcd46d"},
{file = "protobuf-5.26.0-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e184175276edc222e2d5e314a72521e10049938a9a4961fe4bea9b25d073c03f"}, {file = "protobuf-5.26.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:fbfe61e7ee8c1860855696e3ac6cfd1b01af5498facc6834fcc345c9684fb2ca"},
{file = "protobuf-5.26.0-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:6ee9d1aa02f951c5ce10bf8c6cfb7604133773038e33f913183c8b5201350600"}, {file = "protobuf-5.26.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:f7417703f841167e5a27d48be13389d52ad705ec09eade63dfc3180a959215d7"},
{file = "protobuf-5.26.0-cp38-cp38-win32.whl", hash = "sha256:2c334550e1cb4efac5c8a3987384bf13a4334abaf5ab59e40479e7b70ecd6b19"}, {file = "protobuf-5.26.1-cp38-cp38-win32.whl", hash = "sha256:d693d2504ca96750d92d9de8a103102dd648fda04540495535f0fec7577ed8fc"},
{file = "protobuf-5.26.0-cp38-cp38-win_amd64.whl", hash = "sha256:8eef61a90631c21b06b4f492a27e199a269827f046de3bb68b61aa84fcf50905"}, {file = "protobuf-5.26.1-cp38-cp38-win_amd64.whl", hash = "sha256:9b557c317ebe6836835ec4ef74ec3e994ad0894ea424314ad3552bc6e8835b4e"},
{file = "protobuf-5.26.0-cp39-cp39-win32.whl", hash = "sha256:ca825f4eecb8c342d2ec581e6a5ad1ad1a47bededaecd768e0d3451ae4aaac2b"}, {file = "protobuf-5.26.1-cp39-cp39-win32.whl", hash = "sha256:b9ba3ca83c2e31219ffbeb9d76b63aad35a3eb1544170c55336993d7a18ae72c"},
{file = "protobuf-5.26.0-cp39-cp39-win_amd64.whl", hash = "sha256:efd4f5894c50bd76cbcfdd668cd941021333861ed0f441c78a83d8254a01cc9f"}, {file = "protobuf-5.26.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ee014c2c87582e101d6b54260af03b6596728505c79f17c8586e7523aaa8f8c"},
{file = "protobuf-5.26.0-py3-none-any.whl", hash = "sha256:a49b6c5359bf34fb7bf965bf21abfab4476e4527d822ab5289ee3bf73f291159"}, {file = "protobuf-5.26.1-py3-none-any.whl", hash = "sha256:da612f2720c0183417194eeaa2523215c4fcc1a1949772dc65f05047e08d5932"},
{file = "protobuf-5.26.0.tar.gz", hash = "sha256:82f5870d74c99addfe4152777bdf8168244b9cf0ac65f8eccf045ddfa9d80d9b"}, {file = "protobuf-5.26.1.tar.gz", hash = "sha256:8ca2a1d97c290ec7b16e4e5dff2e5ae150cc1582f55b5ab300d45cb0dfa90e51"},
] ]
[[package]] [[package]]
@ -3246,13 +3289,13 @@ files = [
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.21" version = "2.22"
description = "C parser in Python" description = "C parser in Python"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=3.8"
files = [ files = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
] ]
[[package]] [[package]]
@ -6511,13 +6554,13 @@ testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygm
[[package]] [[package]]
name = "pytest-cov" name = "pytest-cov"
version = "4.1.0" version = "5.0.0"
description = "Pytest plugin for measuring coverage." description = "Pytest plugin for measuring coverage."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
{file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
] ]
[package.dependencies] [package.dependencies]
@ -6525,7 +6568,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]}
pytest = ">=4.6" pytest = ">=4.6"
[package.extras] [package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
[[package]] [[package]]
name = "pytest-cpp" name = "pytest-cpp"
@ -6645,13 +6688,13 @@ files = [
[[package]] [[package]]
name = "pytools" name = "pytools"
version = "2023.1.1" version = "2024.1.1"
description = "A collection of tools for Python" description = "A collection of tools for Python"
optional = false optional = false
python-versions = "~=3.8" python-versions = "~=3.8"
files = [ files = [
{file = "pytools-2023.1.1-py2.py3-none-any.whl", hash = "sha256:53b98e5d6c01a90e343f8be2f5271e94204a210ef3e74fbefa3d47ec7480f150"}, {file = "pytools-2024.1.1-py2.py3-none-any.whl", hash = "sha256:9f1d03040d78d9d2a607d08de64ec4e350aecdf4ee019f627ce1f1f0c2a4400d"},
{file = "pytools-2023.1.1.tar.gz", hash = "sha256:80637873d206f6bcedf7cdb46ad93e868acb4ea2256db052dfcca872bdd0321f"}, {file = "pytools-2024.1.1.tar.gz", hash = "sha256:2c88edfa990c8e325167c37659fb1e10a3c1133dfaf752bbd7d8456402b8dcff"},
] ]
[package.dependencies] [package.dependencies]
@ -6947,28 +6990,28 @@ docs = ["furo (==2023.9.10)", "pyenchant (==3.2.2)", "sphinx (==7.1.2)", "sphinx
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.3.3" version = "0.3.4"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"},
{file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"},
{file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"},
{file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"},
{file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"},
{file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"},
{file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"},
{file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"},
{file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"},
{file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"},
{file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"},
{file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"},
{file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"},
{file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"},
{file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"},
{file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"},
{file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"},
] ]
[[package]] [[package]]
@ -7047,13 +7090,13 @@ stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"]
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "1.42.0" version = "1.44.0"
description = "Python client for Sentry (https://sentry.io)" description = "Python client for Sentry (https://sentry.io)"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "sentry-sdk-1.42.0.tar.gz", hash = "sha256:4a8364b8f7edbf47f95f7163e48334c96100d9c098f0ae6606e2e18183c223e6"}, {file = "sentry-sdk-1.44.0.tar.gz", hash = "sha256:f7125a9235795811962d52ff796dc032cd1d0dd98b59beaced8380371cd9c13c"},
{file = "sentry_sdk-1.42.0-py2.py3-none-any.whl", hash = "sha256:a654ee7e497a3f5f6368b36d4f04baeab1fe92b3105f7f6965d6ef0de35a9ba4"}, {file = "sentry_sdk-1.44.0-py2.py3-none-any.whl", hash = "sha256:eb65289da013ca92fad2694851ad2f086aa3825e808dc285bd7dcaf63602bb18"},
] ]
[package.dependencies] [package.dependencies]
@ -7067,6 +7110,7 @@ asyncpg = ["asyncpg (>=0.23)"]
beam = ["apache-beam (>=2.12)"] beam = ["apache-beam (>=2.12)"]
bottle = ["bottle (>=0.12.13)"] bottle = ["bottle (>=0.12.13)"]
celery = ["celery (>=3)"] celery = ["celery (>=3)"]
celery-redbeat = ["celery-redbeat (>=2)"]
chalice = ["chalice (>=1.16.0)"] chalice = ["chalice (>=1.16.0)"]
clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] clickhouse-driver = ["clickhouse-driver (>=0.2.0)"]
django = ["django (>=1.8)"] django = ["django (>=1.8)"]

@ -0,0 +1,44 @@
# openpilot releases
## terms
- `channel` - a named version of openpilot (git branch, casync caibx) which receives updates
- `build` - a release which is already built for the comma 3/3x and contains only required files for running openpilot and identifying the release
- `build_style` - type of build, either `debug` or `release`
- `debug` - build with `ALLOW_DEBUG=true`, can test experimental features like longitudinal on alpha cars
- `release` - build with `ALLOW_DEBUG=false`, experimental features disabled
## openpilot channels
| channel | build_style | description |
| ----------- | ----------- | ---------- |
| release | `release` | stable release of openpilot |
| staging | `release` | release candidate of openpilot for final verification |
| nightly | `release` | generated nightly from last commit passing CI tests |
| master | `debug` | current master commit with experimental features enabled |
| git branches | `debug` | installed manually, experimental features enabled, build required |
## creating casync build
`create_casync_build.sh` - creates a casync openpilot build, ready to upload to `openpilot-releases`
```bash
# run on a tici, within the directory you want to create the build from.
# creates a prebuilt version of openpilot into BUILD_DIR and outputs the caibx
# of a tarball containing the full prebuilt openpilot release
BUILD_DIR=/data/openpilot_build \
CASYNC_DIR=/data/casync \
OPENPILOT_CHANNEL=nightly \
release/create_casync_build.sh
```
`upload_casync_release.sh` - helper for uploading a casync build to `openpilot-releases`
## release builds
to create a release build, set `RELEASE=1` environment variable when running the build script

@ -0,0 +1,15 @@
#!/bin/bash
SOURCE_DIR=$1
TARGET_DIR=$2
if [ -f /TICI ]; then
FILES_SRC="release/files_tici"
else
echo "no release files set"
exit 1
fi
cd $SOURCE_DIR
cp -pR --parents $(cat release/files_common) $TARGET_DIR/
cp -pR --parents $(cat $FILES_SRC) $TARGET_DIR/

@ -0,0 +1,22 @@
#!/usr/bin/bash
set -ex
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
CASYNC_DIR="${CASYNC_DIR:=/tmp/casync}"
SOURCE_DIR="$(git -C $DIR rev-parse --show-toplevel)"
BUILD_DIR="${BUILD_DIR:=$(mktemp -d)}"
echo "Creating casync release from $SOURCE_DIR to $CASYNC_DIR"
cd $SOURCE_DIR
mkdir -p $CASYNC_DIR
rm -rf $BUILD_DIR
mkdir -p $BUILD_DIR
release/copy_build_files.sh $SOURCE_DIR $BUILD_DIR
release/create_prebuilt.sh $BUILD_DIR
cd $SOURCE_DIR
release/create_casync_release.py $BUILD_DIR $CASYNC_DIR $OPENPILOT_CHANNEL

@ -0,0 +1,28 @@
#!/usr/bin/env python
import argparse
import os
import pathlib
from openpilot.system.updated.casync.common import create_casync_release, create_build_metadata_file
from openpilot.system.version import get_build_metadata
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="creates a casync release")
parser.add_argument("target_dir", type=str, help="target directory to build channel from")
parser.add_argument("output_dir", type=str, help="output directory for the channel")
parser.add_argument("channel", type=str, help="what channel this build is")
args = parser.parse_args()
target_dir = pathlib.Path(args.target_dir)
output_dir = pathlib.Path(args.output_dir)
build_metadata = get_build_metadata()
build_metadata.openpilot.build_style = "release" if os.environ.get("RELEASE", None) is not None else "debug"
create_build_metadata_file(target_dir, build_metadata, args.channel)
digest, caibx = create_casync_release(target_dir, output_dir, build_metadata.canonical)
print(f"Created casync release from {target_dir} to {caibx} with digest {digest}")

@ -0,0 +1,34 @@
#!/usr/bin/bash -e
# runs on tici to create a prebuilt version of a release
set -ex
BUILD_DIR=$1
cd $BUILD_DIR
# Build
export PYTHONPATH="$BUILD_DIR"
rm -f panda/board/obj/panda.bin.signed
rm -f panda/board/obj/panda_h7.bin.signed
if [ -n "$RELEASE" ]; then
export CERT=/data/pandaextra/certs/release
fi
scons -j$(nproc)
# Cleanup
find . -name '*.a' -delete
find . -name '*.o' -delete
find . -name '*.os' -delete
find . -name '*.pyc' -delete
find . -name 'moc_*' -delete
find . -name '__pycache__' -delete
rm -rf .sconsign.dblite Jenkinsfile release/
rm selfdrive/modeld/models/supercombo.onnx
# Mark as prebuilt release
touch prebuilt

@ -55,7 +55,7 @@ selfdrive/sentry.py
selfdrive/tombstoned.py selfdrive/tombstoned.py
selfdrive/statsd.py selfdrive/statsd.py
selfdrive/updated/* selfdrive/updated/**
system/logmessaged.py system/logmessaged.py
system/micd.py system/micd.py
@ -170,7 +170,6 @@ system/hardware/tici/hardware.h
system/hardware/tici/hardware.py system/hardware/tici/hardware.py
system/hardware/tici/pins.py system/hardware/tici/pins.py
system/hardware/tici/agnos.py system/hardware/tici/agnos.py
system/hardware/tici/casync.py
system/hardware/tici/agnos.json system/hardware/tici/agnos.json
system/hardware/tici/amplifier.py system/hardware/tici/amplifier.py
system/hardware/tici/updater system/hardware/tici/updater
@ -187,6 +186,8 @@ system/ubloxd/generated/*
system/ubloxd/*.h system/ubloxd/*.h
system/ubloxd/*.cc system/ubloxd/*.cc
system/updated/*
selfdrive/locationd/__init__.py selfdrive/locationd/__init__.py
selfdrive/locationd/SConscript selfdrive/locationd/SConscript
selfdrive/locationd/.gitignore selfdrive/locationd/.gitignore

@ -0,0 +1,9 @@
#!/bin/bash
CASYNC_DIR="${CASYNC_DIR:=/tmp/casync}"
OPENPILOT_RELEASES="https://commadist.blob.core.windows.net/openpilot-releases/"
SAS="$(python -c 'from tools.lib.azure_container import get_container_sas;print(get_container_sas("commadist","openpilot-releases"))')"
azcopy cp "$CASYNC_DIR*" "$OPENPILOT_RELEASES?$SAS" --recursive

@ -32,13 +32,12 @@ from cereal import log
from cereal.services import SERVICE_LIST from cereal.services import SERVICE_LIST
from openpilot.common.api import Api from openpilot.common.api import Api
from openpilot.common.file_helpers import CallbackReader from openpilot.common.file_helpers import CallbackReader
from openpilot.common.git import get_commit, get_normalized_origin, get_short_branch
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.realtime import set_core_affinity from openpilot.common.realtime import set_core_affinity
from openpilot.system.hardware import HARDWARE, PC from openpilot.system.hardware import HARDWARE, PC
from openpilot.system.loggerd.xattr_cache import getxattr, setxattr from openpilot.system.loggerd.xattr_cache import getxattr, setxattr
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.system.version import get_version from openpilot.system.version import get_build_metadata
from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import Paths
@ -320,11 +319,12 @@ def getMessage(service: str, timeout: int = 1000) -> dict:
@dispatcher.add_method @dispatcher.add_method
def getVersion() -> dict[str, str]: def getVersion() -> dict[str, str]:
build_metadata = get_build_metadata()
return { return {
"version": get_version(), "version": build_metadata.openpilot.version,
"remote": get_normalized_origin(), "remote": build_metadata.openpilot.git_normalized_origin,
"branch": get_short_branch(), "branch": build_metadata.channel,
"commit": get_commit(), "commit": build_metadata.openpilot.git_commit,
} }

@ -7,8 +7,7 @@ from openpilot.common.params import Params
from openpilot.selfdrive.manager.process import launcher from openpilot.selfdrive.manager.process import launcher
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import HARDWARE from openpilot.system.hardware import HARDWARE
from openpilot.common.git import get_commit, get_normalized_origin, get_short_branch from openpilot.system.version import get_build_metadata
from openpilot.system.version import get_version, is_dirty
ATHENA_MGR_PID_PARAM = "AthenadPid" ATHENA_MGR_PID_PARAM = "AthenadPid"
@ -16,12 +15,14 @@ ATHENA_MGR_PID_PARAM = "AthenadPid"
def main(): def main():
params = Params() params = Params()
dongle_id = params.get("DongleId").decode('utf-8') dongle_id = params.get("DongleId").decode('utf-8')
build_metadata = get_build_metadata()
cloudlog.bind_global(dongle_id=dongle_id, cloudlog.bind_global(dongle_id=dongle_id,
version=get_version(), version=build_metadata.openpilot.version,
origin=get_normalized_origin(), origin=build_metadata.openpilot.git_normalized_origin,
branch=get_short_branch(), branch=build_metadata.channel,
commit=get_commit(), commit=build_metadata.openpilot.git_commit,
dirty=is_dirty(), dirty=build_metadata.openpilot.is_dirty,
device=HARDWARE.get_device_type()) device=HARDWARE.get_device_type())
try: try:

@ -144,7 +144,7 @@ bool safety_setter_thread(std::vector<Panda *> pandas) {
Panda *connect(std::string serial="", uint32_t index=0) { Panda *connect(std::string serial="", uint32_t index=0) {
std::unique_ptr<Panda> panda; std::unique_ptr<Panda> panda;
try { try {
panda = std::make_unique<Panda>(serial, (index * PANDA_BUS_CNT)); panda = std::make_unique<Panda>(serial, (index * PANDA_BUS_OFFSET));
} catch (std::exception &e) { } catch (std::exception &e) {
return nullptr; return nullptr;
} }
@ -316,7 +316,6 @@ std::optional<bool> send_panda_states(PubMaster *pm, const std::vector<Panda *>
ps.setControlsAllowed(health.controls_allowed_pkt); ps.setControlsAllowed(health.controls_allowed_pkt);
ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt); ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt);
ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt); ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt);
ps.setGmlanSendErrs(health.gmlan_send_errs_pkt);
ps.setPandaType(panda->hw_type); ps.setPandaType(panda->hw_type);
ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt)); ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt));
ps.setSafetyParam(health.safety_param_pkt); ps.setSafetyParam(health.safety_param_pkt);

@ -172,7 +172,7 @@ void Panda::pack_can_buffer(const capnp::List<cereal::CanData>::Reader &can_data
for (auto cmsg : can_data_list) { for (auto cmsg : can_data_list) {
// check if the message is intended for this panda // check if the message is intended for this panda
uint8_t bus = cmsg.getSrc(); uint8_t bus = cmsg.getSrc();
if (bus < bus_offset || bus >= (bus_offset + PANDA_BUS_CNT)) { if (bus < bus_offset || bus >= (bus_offset + PANDA_BUS_OFFSET)) {
continue; continue;
} }
auto can_data = cmsg.getDat(); auto can_data = cmsg.getDat();

@ -23,6 +23,8 @@
#define CAN_REJECTED_BUS_OFFSET 0xC0U #define CAN_REJECTED_BUS_OFFSET 0xC0U
#define CAN_RETURNED_BUS_OFFSET 0x80U #define CAN_RETURNED_BUS_OFFSET 0x80U
#define PANDA_BUS_OFFSET 4
struct __attribute__((packed)) can_header { struct __attribute__((packed)) can_header {
uint8_t reserved : 1; uint8_t reserved : 1;
uint8_t bus : 3; uint8_t bus : 3;

@ -155,10 +155,7 @@ def main() -> NoReturn:
if first_run: if first_run:
# reset panda to ensure we're in a good state # reset panda to ensure we're in a good state
cloudlog.info(f"Resetting panda {panda.get_usb_serial()}") cloudlog.info(f"Resetting panda {panda.get_usb_serial()}")
if panda.is_internal(): panda.reset(reconnect=False)
HARDWARE.reset_internal_panda()
else:
panda.reset(reconnect=False)
for p in pandas: for p in pandas:
p.close() p.close()

@ -40,7 +40,7 @@ PandaTest::PandaTest(uint32_t bus_offset_, int can_list_size, cereal::PandaState
uint32_t id = util::random_int(0, std::size(dlc_to_len) - 1); uint32_t id = util::random_int(0, std::size(dlc_to_len) - 1);
const std::string &dat = test_data[dlc_to_len[id]]; const std::string &dat = test_data[dlc_to_len[id]];
can.setAddress(i); can.setAddress(i);
can.setSrc(util::random_int(0, 3) + bus_offset); can.setSrc(util::random_int(0, 2) + bus_offset);
can.setDat(kj::ArrayPtr((uint8_t *)dat.data(), dat.size())); can.setDat(kj::ArrayPtr((uint8_t *)dat.data(), dat.size()));
total_pakets_size += sizeof(can_header) + dat.size(); total_pakets_size += sizeof(can_header) + dat.size();
} }

@ -23,23 +23,28 @@ class TestPandad(unittest.TestCase):
if len(Panda.list()) == 0: if len(Panda.list()) == 0:
self._run_test(60) self._run_test(60)
self.spi = HARDWARE.get_device_type() != 'tici'
def tearDown(self): def tearDown(self):
managed_processes['pandad'].stop() managed_processes['pandad'].stop()
def _run_test(self, timeout=30): def _run_test(self, timeout=30) -> float:
managed_processes['pandad'].start() st = time.monotonic()
sm = messaging.SubMaster(['pandaStates'])
sm = messaging.SubMaster(['peripheralState']) managed_processes['pandad'].start()
for _ in range(timeout*10): while (time.monotonic() - st) < timeout:
sm.update(100) sm.update(100)
if sm['peripheralState'].pandaType != log.PandaState.PandaType.unknown: if len(sm['pandaStates']) and sm['pandaStates'][0].pandaType != log.PandaState.PandaType.unknown:
break break
dt = time.monotonic() - st
managed_processes['pandad'].stop() managed_processes['pandad'].stop()
if sm['peripheralState'].pandaType == log.PandaState.PandaType.unknown: if len(sm['pandaStates']) == 0 or sm['pandaStates'][0].pandaType == log.PandaState.PandaType.unknown:
raise Exception("boardd failed to start") raise Exception("boardd failed to start")
return dt
def _go_to_dfu(self): def _go_to_dfu(self):
HARDWARE.recover_internal_panda() HARDWARE.recover_internal_panda()
assert Panda.wait_for_dfu(None, 10) assert Panda.wait_for_dfu(None, 10)
@ -88,14 +93,23 @@ class TestPandad(unittest.TestCase):
assert any(Panda(s).is_internal() for s in Panda.list()) assert any(Panda(s).is_internal() for s in Panda.list())
def test_best_case_startup_time(self): def test_best_case_startup_time(self):
# run once so we're setup # run once so we're up to date
self._run_test(60) self._run_test(60)
# should be fast this time ts = []
self._run_test(8) for _ in range(10):
# should be nearly instant this time
dt = self._run_test(5)
ts.append(dt)
# 5s for USB (due to enumeration)
# - 0.2s pandad -> boardd
# - plus some buffer
assert 0.1 < (sum(ts)/len(ts)) < (0.5 if self.spi else 5.0)
print("startup times", ts, sum(ts) / len(ts))
def test_protocol_version_check(self): def test_protocol_version_check(self):
if HARDWARE.get_device_type() == 'tici': if not self.spi:
raise unittest.SkipTest("SPI test") raise unittest.SkipTest("SPI test")
# flash old fw # flash old fw
fn = os.path.join(HERE, "bootstub.panda_h7_spiv0.bin") fn = os.path.join(HERE, "bootstub.panda_h7_spiv0.bin")

@ -165,20 +165,6 @@ def common_fault_avoidance(fault_condition: bool, request: bool, above_limit_fra
return above_limit_frames, request return above_limit_frames, request
def crc8_pedal(data):
crc = 0xFF # standard init value
poly = 0xD5 # standard crc8: x8+x7+x6+x4+x2+1
size = len(data)
for i in range(size - 1, -1, -1):
crc ^= data[i]
for _ in range(8):
if ((crc & 0x80) != 0):
crc = ((crc << 1) ^ poly) & 0xFF
else:
crc <<= 1
return crc
def make_can_msg(addr, dat, bus): def make_can_msg(addr, dat, bus):
return [addr, 0, dat, bus] return [addr, 0, dat, bus]

@ -4,7 +4,6 @@ from collections.abc import Callable
from cereal import car from cereal import car
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.version import is_comma_remote, is_tested_branch
from openpilot.selfdrive.car.interfaces import get_interface_attr from openpilot.selfdrive.car.interfaces import get_interface_attr
from openpilot.selfdrive.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars from openpilot.selfdrive.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars
from openpilot.selfdrive.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN from openpilot.selfdrive.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN
@ -13,6 +12,7 @@ from openpilot.selfdrive.car.mock.values import CAR as MOCK
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
import cereal.messaging as messaging import cereal.messaging as messaging
from openpilot.selfdrive.car import gen_empty_fingerprint from openpilot.selfdrive.car import gen_empty_fingerprint
from openpilot.system.version import get_build_metadata
FRAME_FINGERPRINT = 100 # 1s FRAME_FINGERPRINT = 100 # 1s
@ -20,7 +20,8 @@ EventName = car.CarEvent.EventName
def get_startup_event(car_recognized, controller_available, fw_seen): def get_startup_event(car_recognized, controller_available, fw_seen):
if is_comma_remote() and is_tested_branch(): build_metadata = get_build_metadata()
if build_metadata.openpilot.comma_remote and build_metadata.tested_channel:
event = EventName.startup event = EventName.startup
else: else:
event = EventName.startupMaster event = EventName.startupMaster

@ -81,6 +81,7 @@ FW_VERSIONS = {
b'68277370AJ', b'68277370AJ',
b'68277370AM', b'68277370AM',
b'68277372AD', b'68277372AD',
b'68277372AE',
b'68277372AN', b'68277372AN',
b'68277374AA', b'68277374AA',
b'68277374AB', b'68277374AB',
@ -99,6 +100,7 @@ FW_VERSIONS = {
b'68436250AE', b'68436250AE',
b'68529067AA', b'68529067AA',
b'68594993AB', b'68594993AB',
b'68594994AB',
], ],
(Ecu.srs, 0x744, None): [ (Ecu.srs, 0x744, None): [
b'68405565AB', b'68405565AB',
@ -122,15 +124,18 @@ FW_VERSIONS = {
b'68540436AC', b'68540436AC',
b'68540436AD', b'68540436AD',
b'68598670AB', b'68598670AB',
b'68598670AC',
], ],
(Ecu.eps, 0x75a, None): [ (Ecu.eps, 0x75a, None): [
b'68416742AA', b'68416742AA',
b'68460393AA', b'68460393AA',
b'68460393AB', b'68460393AB',
b'68494461AB', b'68494461AB',
b'68494461AC',
b'68524936AA', b'68524936AA',
b'68524936AB', b'68524936AB',
b'68525338AB', b'68525338AB',
b'68594337AB',
b'68594340AB', b'68594340AB',
], ],
(Ecu.engine, 0x7e0, None): [ (Ecu.engine, 0x7e0, None): [
@ -142,11 +147,14 @@ FW_VERSIONS = {
b'68443120AE ', b'68443120AE ',
b'68443123AC ', b'68443123AC ',
b'68443125AC ', b'68443125AC ',
b'68496647AJ ',
b'68496650AH ',
b'68496650AI ', b'68496650AI ',
b'68526752AD ', b'68526752AD ',
b'68526752AE ', b'68526752AE ',
b'68526754AE ', b'68526754AE ',
b'68536264AE ', b'68536264AE ',
b'68700304AB ',
b'68700306AB ', b'68700306AB ',
], ],
(Ecu.transmission, 0x7e1, None): [ (Ecu.transmission, 0x7e1, None): [
@ -157,9 +165,11 @@ FW_VERSIONS = {
b'68443155AC', b'68443155AC',
b'68443158AB', b'68443158AB',
b'68501050AD', b'68501050AD',
b'68501055AD',
b'68527221AB', b'68527221AB',
b'68527223AB', b'68527223AB',
b'68586231AD', b'68586231AD',
b'68586233AD',
], ],
}, },
CAR.CHRYSLER_PACIFICA_2018_HYBRID: { CAR.CHRYSLER_PACIFICA_2018_HYBRID: {
@ -260,6 +270,7 @@ FW_VERSIONS = {
b'68331511AC', b'68331511AC',
b'68331574AC', b'68331574AC',
b'68331687AC', b'68331687AC',
b'68331690AC',
b'68340272AD', b'68340272AD',
], ],
(Ecu.srs, 0x744, None): [ (Ecu.srs, 0x744, None): [
@ -386,6 +397,7 @@ FW_VERSIONS = {
b'68453491AC', b'68453491AC',
b'68453499AD', b'68453499AD',
b'68453503AC', b'68453503AC',
b'68453503AD',
b'68453505AC', b'68453505AC',
b'68453505AD', b'68453505AD',
b'68453511AC', b'68453511AC',
@ -412,6 +424,7 @@ FW_VERSIONS = {
b'68631938AA', b'68631938AA',
b'68631940AA', b'68631940AA',
b'68631942AA', b'68631942AA',
b'68631943AB',
], ],
(Ecu.srs, 0x744, None): [ (Ecu.srs, 0x744, None): [
b'68428609AB', b'68428609AB',
@ -486,6 +499,7 @@ FW_VERSIONS = {
b'05036026AB ', b'05036026AB ',
b'05036065AE ', b'05036065AE ',
b'05036066AE ', b'05036066AE ',
b'05036193AA ',
b'05149368AA ', b'05149368AA ',
b'05149591AD ', b'05149591AD ',
b'05149591AE ', b'05149591AE ',
@ -526,6 +540,7 @@ FW_VERSIONS = {
b'68502740AF ', b'68502740AF ',
b'68502741AF ', b'68502741AF ',
b'68502742AC ', b'68502742AC ',
b'68502742AF ',
b'68539650AD', b'68539650AD',
b'68539650AF', b'68539650AF',
b'68539651AD', b'68539651AD',
@ -539,6 +554,7 @@ FW_VERSIONS = {
b'05035706AD', b'05035706AD',
b'05035842AB', b'05035842AB',
b'05036069AA', b'05036069AA',
b'05036181AA',
b'05149536AC', b'05149536AC',
b'05149537AC', b'05149537AC',
b'05149543AC', b'05149543AC',

@ -340,7 +340,7 @@ class CarDocs:
# experimental mode # experimental mode
exp_link = "<a href='https://blog.comma.ai/090release/#experimental-mode' target='_blank' class='link-light-new-regular-text'>Experimental mode</a>" exp_link = "<a href='https://blog.comma.ai/090release/#experimental-mode' target='_blank' class='link-light-new-regular-text'>Experimental mode</a>"
if CP.openpilotLongitudinalControl or CP.experimentalLongitudinalAvailable: if CP.openpilotLongitudinalControl and not CP.experimentalLongitudinalAvailable:
sentence_builder += f" Traffic light and stop sign handling is also available in {exp_link}." sentence_builder += f" Traffic light and stop sign handling is also available in {exp_link}."
return sentence_builder.format(car_model=f"{self.make} {self.model}", alc=alc, acc=acc) return sentence_builder.format(car_model=f"{self.make} {self.model}", alc=alc, acc=acc)

@ -6,6 +6,7 @@ from openpilot.selfdrive.car.gm.values import CAR as GM
from openpilot.selfdrive.car.honda.values import CAR as HONDA from openpilot.selfdrive.car.honda.values import CAR as HONDA
from openpilot.selfdrive.car.hyundai.values import CAR as HYUNDAI from openpilot.selfdrive.car.hyundai.values import CAR as HYUNDAI
from openpilot.selfdrive.car.mazda.values import CAR as MAZDA from openpilot.selfdrive.car.mazda.values import CAR as MAZDA
from openpilot.selfdrive.car.mock.values import CAR as MOCK
from openpilot.selfdrive.car.nissan.values import CAR as NISSAN from openpilot.selfdrive.car.nissan.values import CAR as NISSAN
from openpilot.selfdrive.car.subaru.values import CAR as SUBARU from openpilot.selfdrive.car.subaru.values import CAR as SUBARU
from openpilot.selfdrive.car.tesla.values import CAR as TESLA from openpilot.selfdrive.car.tesla.values import CAR as TESLA
@ -134,6 +135,8 @@ MIGRATION = {
"CHRYSLER PACIFICA 2018": CHRYSLER.CHRYSLER_PACIFICA_2018, "CHRYSLER PACIFICA 2018": CHRYSLER.CHRYSLER_PACIFICA_2018,
"CHRYSLER PACIFICA 2020": CHRYSLER.CHRYSLER_PACIFICA_2020, "CHRYSLER PACIFICA 2020": CHRYSLER.CHRYSLER_PACIFICA_2020,
"DODGE DURANGO 2021": CHRYSLER.DODGE_DURANGO, "DODGE DURANGO 2021": CHRYSLER.DODGE_DURANGO,
"JEEP GRAND CHEROKEE V6 2018": CHRYSLER.JEEP_GRAND_CHEROKEE,
"JEEP GRAND CHEROKEE 2019": CHRYSLER.JEEP_GRAND_CHEROKEE_2019,
"RAM 1500 5TH GEN": CHRYSLER.RAM_1500_5TH_GEN, "RAM 1500 5TH GEN": CHRYSLER.RAM_1500_5TH_GEN,
"RAM HD 5TH GEN": CHRYSLER.RAM_HD_5TH_GEN, "RAM HD 5TH GEN": CHRYSLER.RAM_HD_5TH_GEN,
"FORD BRONCO SPORT 1ST GEN": FORD.FORD_BRONCO_SPORT_MK1, "FORD BRONCO SPORT 1ST GEN": FORD.FORD_BRONCO_SPORT_MK1,
@ -253,6 +256,7 @@ MIGRATION = {
"MAZDA CX-5 2022": MAZDA.MAZDA_CX5_2022, "MAZDA CX-5 2022": MAZDA.MAZDA_CX5_2022,
"NISSAN X-TRAIL 2017": NISSAN.NISSAN_XTRAIL, "NISSAN X-TRAIL 2017": NISSAN.NISSAN_XTRAIL,
"NISSAN LEAF 2018": NISSAN.NISSAN_LEAF, "NISSAN LEAF 2018": NISSAN.NISSAN_LEAF,
"NISSAN LEAF 2018 Instrument Cluster": NISSAN.NISSAN_LEAF_IC,
"NISSAN ROGUE 2019": NISSAN.NISSAN_ROGUE, "NISSAN ROGUE 2019": NISSAN.NISSAN_ROGUE,
"NISSAN ALTIMA 2020": NISSAN.NISSAN_ALTIMA, "NISSAN ALTIMA 2020": NISSAN.NISSAN_ALTIMA,
"SUBARU ASCENT LIMITED 2019": SUBARU.SUBARU_ASCENT, "SUBARU ASCENT LIMITED 2019": SUBARU.SUBARU_ASCENT,
@ -335,4 +339,6 @@ MIGRATION = {
"SKODA OCTAVIA 3RD GEN": VW.SKODA_OCTAVIA_MK3, "SKODA OCTAVIA 3RD GEN": VW.SKODA_OCTAVIA_MK3,
"SKODA SCALA 1ST GEN": VW.SKODA_SCALA_MK1, "SKODA SCALA 1ST GEN": VW.SKODA_SCALA_MK1,
"SKODA SUPERB 3RD GEN": VW.SKODA_SUPERB_MK3, "SKODA SUPERB 3RD GEN": VW.SKODA_SUPERB_MK3,
"mock": MOCK.MOCK,
} }

@ -57,7 +57,8 @@ class CarState(CarStateBase):
ret.steerFaultTemporary |= cp.vl["Lane_Assist_Data3_FD1"]["LatCtlSte_D_Stat"] not in (1, 2, 3) ret.steerFaultTemporary |= cp.vl["Lane_Assist_Data3_FD1"]["LatCtlSte_D_Stat"] not in (1, 2, 3)
# cruise state # cruise state
ret.cruiseState.speed = cp.vl["EngBrakeData"]["Veh_V_DsplyCcSet"] * CV.MPH_TO_MS is_metric = cp.vl["INSTRUMENT_PANEL"]["METRIC_UNITS"] == 1 if not self.CP.flags & FordFlags.CANFD else False
ret.cruiseState.speed = cp.vl["EngBrakeData"]["Veh_V_DsplyCcSet"] * (CV.KPH_TO_MS if is_metric else CV.MPH_TO_MS)
ret.cruiseState.enabled = cp.vl["EngBrakeData"]["CcStat_D_Actl"] in (4, 5) ret.cruiseState.enabled = cp.vl["EngBrakeData"]["CcStat_D_Actl"] in (4, 5)
ret.cruiseState.available = cp.vl["EngBrakeData"]["CcStat_D_Actl"] in (3, 4, 5) ret.cruiseState.available = cp.vl["EngBrakeData"]["CcStat_D_Actl"] in (3, 4, 5)
ret.cruiseState.nonAdaptive = cp.vl["Cluster_Info1_FD1"]["AccEnbl_B_RqDrv"] == 0 ret.cruiseState.nonAdaptive = cp.vl["Cluster_Info1_FD1"]["AccEnbl_B_RqDrv"] == 0
@ -131,6 +132,10 @@ class CarState(CarStateBase):
messages += [ messages += [
("Lane_Assist_Data3_FD1", 33), ("Lane_Assist_Data3_FD1", 33),
] ]
else:
messages += [
("INSTRUMENT_PANEL", 1),
]
if CP.transmissionType == TransmissionType.automatic: if CP.transmissionType == TransmissionType.automatic:
messages += [ messages += [

@ -131,9 +131,11 @@ FW_VERSIONS = {
}, },
CAR.FORD_MAVERICK_MK1: { CAR.FORD_MAVERICK_MK1: {
(Ecu.eps, 0x730, None): [ (Ecu.eps, 0x730, None): [
b'NZ6C-14D003-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'NZ6C-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'NZ6C-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.abs, 0x760, None): [ (Ecu.abs, 0x760, None): [
b'NZ6C-2D053-AE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'NZ6C-2D053-AG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'NZ6C-2D053-AG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'PZ6C-2D053-ED\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PZ6C-2D053-ED\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'PZ6C-2D053-EE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PZ6C-2D053-EE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',

@ -388,7 +388,8 @@ if __name__ == "__main__":
padding = max([len(fw.brand) for fw in fw_vers] or [0]) padding = max([len(fw.brand) for fw in fw_vers] or [0])
for version in fw_vers: for version in fw_vers:
subaddr = None if version.subAddress == 0 else hex(version.subAddress) subaddr = None if version.subAddress == 0 else hex(version.subAddress)
print(f" Brand: {version.brand:{padding}}, bus: {version.bus} - (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}]") print(f" Brand: {version.brand:{padding}}, bus: {version.bus}, OBD: {version.obdMultiplexing} - " +
f"(Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}]")
print("}") print("}")
print() print()

@ -576,6 +576,7 @@ FW_VERSIONS = {
b'37805-5PA-AB10\x00\x00', b'37805-5PA-AB10\x00\x00',
b'37805-5PA-AD10\x00\x00', b'37805-5PA-AD10\x00\x00',
b'37805-5PA-AF20\x00\x00', b'37805-5PA-AF20\x00\x00',
b'37805-5PA-AF30\x00\x00',
b'37805-5PA-AH20\x00\x00', b'37805-5PA-AH20\x00\x00',
b'37805-5PA-BF10\x00\x00', b'37805-5PA-BF10\x00\x00',
b'37805-5PA-C680\x00\x00', b'37805-5PA-C680\x00\x00',
@ -1030,6 +1031,7 @@ FW_VERSIONS = {
b'78109-TG7-AS20\x00\x00', b'78109-TG7-AS20\x00\x00',
b'78109-TG7-AT20\x00\x00', b'78109-TG7-AT20\x00\x00',
b'78109-TG7-AU20\x00\x00', b'78109-TG7-AU20\x00\x00',
b'78109-TG7-AW20\x00\x00',
b'78109-TG7-AX20\x00\x00', b'78109-TG7-AX20\x00\x00',
b'78109-TG7-D020\x00\x00', b'78109-TG7-D020\x00\x00',
b'78109-TG7-DJ10\x00\x00', b'78109-TG7-DJ10\x00\x00',

@ -156,7 +156,7 @@ class CAR(Platforms):
flags=HondaFlags.BOSCH_ALT_BRAKE, flags=HondaFlags.BOSCH_ALT_BRAKE,
) )
HONDA_CRV_HYBRID = HondaBoschPlatformConfig( HONDA_CRV_HYBRID = HondaBoschPlatformConfig(
[HondaCarDocs("Honda CR-V Hybrid 2017-20", min_steer_speed=12. * CV.MPH_TO_MS)], [HondaCarDocs("Honda CR-V Hybrid 2017-21", min_steer_speed=12. * CV.MPH_TO_MS)],
# mass: mean of 4 models in kg, steerRatio: 12.3 is spec end-to-end # mass: mean of 4 models in kg, steerRatio: 12.3 is spec end-to-end
CarSpecs(mass=1667, wheelbase=2.66, steerRatio=16, centerToFrontRatio=0.41, tireStiffnessFactor=0.677), CarSpecs(mass=1667, wheelbase=2.66, steerRatio=16, centerToFrontRatio=0.41, tireStiffnessFactor=0.677),
dbc_dict('honda_accord_2018_can_generated', None), dbc_dict('honda_accord_2018_can_generated', None),
@ -261,9 +261,9 @@ class CAR(Platforms):
) )
HONDA_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \ HONDA_ALT_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
p16(0xF112) p16(0xF112)
HONDA_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \ HONDA_ALT_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
p16(0xF112) p16(0xF112)
FW_QUERY_CONFIG = FwQueryConfig( FW_QUERY_CONFIG = FwQueryConfig(
@ -276,17 +276,10 @@ FW_QUERY_CONFIG = FwQueryConfig(
), ),
# Data collection requests: # Data collection requests:
# Attempt to get the radarless Civic 2022+ camera FW # Log manufacturer-specific identifier for current ECUs
Request( Request(
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.UDS_VERSION_REQUEST], [HONDA_ALT_VERSION_REQUEST],
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.UDS_VERSION_RESPONSE], [HONDA_ALT_VERSION_RESPONSE],
bus=0,
logging=True
),
# Log extra identifiers for current ECUs
Request(
[HONDA_VERSION_REQUEST],
[HONDA_VERSION_RESPONSE],
bus=1, bus=1,
logging=True, logging=True,
), ),
@ -307,20 +300,30 @@ FW_QUERY_CONFIG = FwQueryConfig(
# We lose these ECUs without the comma power on these cars. # We lose these ECUs without the comma power on these cars.
# Note that we still attempt to match with them when they are present # Note that we still attempt to match with them when they are present
non_essential_ecus={ non_essential_ecus={
Ecu.programmedFuelInjection: [CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CRV_5G], Ecu.programmedFuelInjection: [CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_2022, CAR.HONDA_CRV_5G,
Ecu.transmission: [CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CRV_5G], CAR.HONDA_PILOT],
Ecu.srs: [CAR.HONDA_ACCORD], Ecu.transmission: [CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_2022, CAR.HONDA_CRV_5G, CAR.HONDA_PILOT],
Ecu.eps: [CAR.HONDA_ACCORD], Ecu.srs: [CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_2022, CAR.HONDA_E],
Ecu.vsa: [CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CRV_5G], Ecu.eps: [CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_2022, CAR.HONDA_E],
Ecu.combinationMeter: [CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CRV_5G], Ecu.vsa: [CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_2022, CAR.HONDA_CRV_5G, CAR.HONDA_CRV_HYBRID,
Ecu.gateway: [CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CRV_5G], CAR.HONDA_E, CAR.HONDA_INSIGHT],
Ecu.electricBrakeBooster: [CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CRV_5G], Ecu.combinationMeter: [CAR.ACURA_ILX, CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_2022, CAR.HONDA_FIT,
Ecu.shiftByWire: [CAR.HONDA_ACCORD], # existence correlates with transmission type for ICE CAR.HONDA_HRV, CAR.HONDA_CRV_5G, CAR.HONDA_CRV_HYBRID, CAR.HONDA_E, CAR.HONDA_INSIGHT, CAR.HONDA_ODYSSEY_CHN],
Ecu.hud: [CAR.HONDA_ACCORD], # existence correlates with trim level Ecu.gateway: [CAR.ACURA_ILX, CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_2022, CAR.HONDA_FIT,
CAR.HONDA_FREED, CAR.HONDA_HRV, CAR.HONDA_RIDGELINE, CAR.HONDA_CRV_5G, CAR.HONDA_CRV_HYBRID, CAR.HONDA_E, CAR.HONDA_INSIGHT,
CAR.HONDA_ODYSSEY, CAR.HONDA_ODYSSEY_CHN, CAR.HONDA_PILOT],
Ecu.electricBrakeBooster: [CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CRV_5G],
# existence correlates with transmission type for Accord ICE
Ecu.shiftByWire: [CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CRV_HYBRID, CAR.HONDA_E, CAR.HONDA_INSIGHT, CAR.HONDA_PILOT],
# existence correlates with trim level
Ecu.hud: [CAR.HONDA_ACCORD],
}, },
extra_ecus=[ extra_ecus=[
# The only other ECU on PT bus accessible by camera on radarless Civic # The only other ECU on PT bus accessible by camera on radarless Civic
(Ecu.unknown, 0x18DAB3F1, None), # This is likely a manufacturer-specific sub-address implementation: the camera responds to this and 0x18dab0f1
# Unclear what the part number refers to: 8S103 is 'Camera Set Mono', while 36160 is 'Camera Monocular - Honda'
# TODO: add query back, camera does not support querying both in parallel and 0x18dab0f1 often fails to respond
# (Ecu.unknown, 0x18DAB3F1, None),
], ],
) )

File diff suppressed because it is too large Load Diff

@ -36,6 +36,8 @@ NO_DATES_PLATFORMS = {
CAR.HYUNDAI_VELOSTER, CAR.HYUNDAI_VELOSTER,
} }
CANFD_EXPECTED_ECUS = {Ecu.fwdCamera, Ecu.fwdRadar}
class TestHyundaiFingerprint(unittest.TestCase): class TestHyundaiFingerprint(unittest.TestCase):
def test_can_features(self): def test_can_features(self):
@ -51,16 +53,14 @@ class TestHyundaiFingerprint(unittest.TestCase):
self.assertEqual(HYBRID_CAR & EV_CAR, set(), "Shared cars between hybrid and EV") self.assertEqual(HYBRID_CAR & EV_CAR, set(), "Shared cars between hybrid and EV")
self.assertEqual(CANFD_CAR & HYBRID_CAR, set(), "Hard coding CAN FD cars as hybrid is no longer supported") self.assertEqual(CANFD_CAR & HYBRID_CAR, set(), "Hard coding CAN FD cars as hybrid is no longer supported")
def test_auxiliary_request_ecu_whitelist(self): def test_canfd_ecu_whitelist(self):
# Asserts only auxiliary Ecus can exist in database for CAN-FD cars # Asserts only expected Ecus can exist in database for CAN-FD cars
whitelisted_ecus = {ecu for r in FW_QUERY_CONFIG.requests for ecu in r.whitelist_ecus if r.auxiliary}
for car_model in CANFD_CAR: for car_model in CANFD_CAR:
ecus = {fw[0] for fw in FW_VERSIONS[car_model].keys()} ecus = {fw[0] for fw in FW_VERSIONS[car_model].keys()}
ecus_not_in_whitelist = ecus - whitelisted_ecus ecus_not_in_whitelist = ecus - CANFD_EXPECTED_ECUS
ecu_strings = ", ".join([f"Ecu.{ECU_NAME[ecu]}" for ecu in ecus_not_in_whitelist]) ecu_strings = ", ".join([f"Ecu.{ECU_NAME[ecu]}" for ecu in ecus_not_in_whitelist])
self.assertEqual(len(ecus_not_in_whitelist), 0, self.assertEqual(len(ecus_not_in_whitelist), 0,
f"{car_model}: Car model has ECUs not in auxiliary request whitelists: {ecu_strings}") f"{car_model}: Car model has unexpected ECUs: {ecu_strings}")
def test_blacklisted_parts(self): def test_blacklisted_parts(self):
# Asserts no ECUs known to be shared across platforms exist in the database. # Asserts no ECUs known to be shared across platforms exist in the database.
@ -85,10 +85,8 @@ class TestHyundaiFingerprint(unittest.TestCase):
for car_model, ecus in FW_VERSIONS.items(): for car_model, ecus in FW_VERSIONS.items():
with self.subTest(car_model=car_model.value): with self.subTest(car_model=car_model.value):
for ecu, fws in ecus.items(): for ecu, fws in ecus.items():
# TODO: enable for Ecu.fwdRadar, Ecu.abs, Ecu.eps, Ecu.transmission self.assertTrue(all(fw.startswith(expected_fw_prefix) for fw in fws),
if ecu[0] in (Ecu.fwdCamera,): f"FW from unexpected request in database: {(ecu, fws)}")
self.assertTrue(all(fw.startswith(expected_fw_prefix) for fw in fws),
f"FW from unexpected request in database: {(ecu, fws)}")
@settings(max_examples=100) @settings(max_examples=100)
@given(data=st.data()) @given(data=st.data())

@ -508,7 +508,12 @@ class CAR(Platforms):
flags=HyundaiFlags.LEGACY, flags=HyundaiFlags.LEGACY,
) )
GENESIS_G70_2020 = HyundaiPlatformConfig( GENESIS_G70_2020 = HyundaiPlatformConfig(
[HyundaiCarDocs("Genesis G70 2020-23", "All", car_parts=CarParts.common([CarHarness.hyundai_f]))], [
# TODO: 2021 MY harness is unknown
HyundaiCarDocs("Genesis G70 2020-21", "All", car_parts=CarParts.common([CarHarness.hyundai_f])),
# TODO: From 3.3T Sport Advanced 2022 & Prestige 2023 Trim, 2.0T is unknown
HyundaiCarDocs("Genesis G70 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_l])),
],
CarSpecs(mass=3673 * CV.LB_TO_KG, wheelbase=2.83, steerRatio=12.9), CarSpecs(mass=3673 * CV.LB_TO_KG, wheelbase=2.83, steerRatio=12.9),
flags=HyundaiFlags.MANDO_RADAR, flags=HyundaiFlags.MANDO_RADAR,
) )
@ -619,11 +624,6 @@ HYUNDAI_VERSION_REQUEST_LONG = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER])
HYUNDAI_VERSION_REQUEST_ALT = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \ HYUNDAI_VERSION_REQUEST_ALT = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
p16(0xf110) # Alt long description p16(0xf110) # Alt long description
HYUNDAI_VERSION_REQUEST_MULTI = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
p16(uds.DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_SPARE_PART_NUMBER) + \
p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION) + \
p16(0xf100)
HYUNDAI_ECU_MANUFACTURING_DATE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \ HYUNDAI_ECU_MANUFACTURING_DATE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
p16(uds.DATA_IDENTIFIER_TYPE.ECU_MANUFACTURING_DATE) p16(uds.DATA_IDENTIFIER_TYPE.ECU_MANUFACTURING_DATE)
@ -647,37 +647,25 @@ PLATFORM_CODE_ECUS = [Ecu.fwdRadar, Ecu.fwdCamera, Ecu.eps]
# TODO: there are date codes in the ABS firmware versions in hex # TODO: there are date codes in the ABS firmware versions in hex
DATE_FW_ECUS = [Ecu.fwdCamera] DATE_FW_ECUS = [Ecu.fwdCamera]
ALL_HYUNDAI_ECUS = [Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.engine, Ecu.parkingAdas,
Ecu.transmission, Ecu.adas, Ecu.hvac, Ecu.cornerRadar, Ecu.combinationMeter]
FW_QUERY_CONFIG = FwQueryConfig( FW_QUERY_CONFIG = FwQueryConfig(
requests=[ requests=[
# TODO: minimize shared whitelists for CAN and cornerRadar for CAN-FD # TODO: add back whitelists
# CAN queries (OBD-II port) # CAN queries (OBD-II port)
Request( Request(
[HYUNDAI_VERSION_REQUEST_LONG], [HYUNDAI_VERSION_REQUEST_LONG],
[HYUNDAI_VERSION_RESPONSE], [HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.transmission, Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera],
),
Request(
[HYUNDAI_VERSION_REQUEST_MULTI],
[HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.engine, Ecu.transmission, Ecu.eps, Ecu.abs, Ecu.fwdRadar],
), ),
# CAN-FD queries (from camera) # CAN & CAN-FD queries (from camera)
# TODO: combine shared whitelists with CAN requests
Request( Request(
[HYUNDAI_VERSION_REQUEST_LONG], [HYUNDAI_VERSION_REQUEST_LONG],
[HYUNDAI_VERSION_RESPONSE], [HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.fwdCamera, Ecu.fwdRadar, Ecu.cornerRadar, Ecu.hvac, Ecu.eps],
bus=0, bus=0,
auxiliary=True, auxiliary=True,
), ),
Request( Request(
[HYUNDAI_VERSION_REQUEST_LONG], [HYUNDAI_VERSION_REQUEST_LONG],
[HYUNDAI_VERSION_RESPONSE], [HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.fwdCamera, Ecu.adas, Ecu.cornerRadar, Ecu.hvac],
bus=1, bus=1,
auxiliary=True, auxiliary=True,
obd_multiplexing=False, obd_multiplexing=False,
@ -688,44 +676,15 @@ FW_QUERY_CONFIG = FwQueryConfig(
Request( Request(
[HYUNDAI_ECU_MANUFACTURING_DATE], [HYUNDAI_ECU_MANUFACTURING_DATE],
[HYUNDAI_VERSION_RESPONSE], [HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.fwdCamera],
bus=0,
auxiliary=True,
logging=True,
),
# CAN & CAN FD logging queries (from camera)
Request(
[HYUNDAI_VERSION_REQUEST_LONG],
[HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=ALL_HYUNDAI_ECUS,
bus=0, bus=0,
auxiliary=True, auxiliary=True,
logging=True, logging=True,
), ),
Request(
[HYUNDAI_VERSION_REQUEST_MULTI],
[HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=ALL_HYUNDAI_ECUS,
bus=0,
auxiliary=True,
logging=True,
),
Request(
[HYUNDAI_VERSION_REQUEST_LONG],
[HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=ALL_HYUNDAI_ECUS,
bus=1,
auxiliary=True,
obd_multiplexing=False,
logging=True,
),
# CAN-FD alt request logging queries # CAN-FD alt request logging queries for hvac and parkingAdas
Request( Request(
[HYUNDAI_VERSION_REQUEST_ALT], [HYUNDAI_VERSION_REQUEST_ALT],
[HYUNDAI_VERSION_RESPONSE], [HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.parkingAdas, Ecu.hvac],
bus=0, bus=0,
auxiliary=True, auxiliary=True,
logging=True, logging=True,
@ -733,7 +692,6 @@ FW_QUERY_CONFIG = FwQueryConfig(
Request( Request(
[HYUNDAI_VERSION_REQUEST_ALT], [HYUNDAI_VERSION_REQUEST_ALT],
[HYUNDAI_VERSION_RESPONSE], [HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.parkingAdas, Ecu.hvac],
bus=1, bus=1,
auxiliary=True, auxiliary=True,
logging=True, logging=True,
@ -743,9 +701,9 @@ FW_QUERY_CONFIG = FwQueryConfig(
# We lose these ECUs without the comma power on these cars. # We lose these ECUs without the comma power on these cars.
# Note that we still attempt to match with them when they are present # Note that we still attempt to match with them when they are present
non_essential_ecus={ non_essential_ecus={
Ecu.transmission: [CAR.HYUNDAI_AZERA_6TH_GEN, CAR.HYUNDAI_AZERA_HEV_6TH_GEN, CAR.HYUNDAI_PALISADE, CAR.HYUNDAI_SONATA], Ecu.abs: [CAR.HYUNDAI_PALISADE, CAR.HYUNDAI_SONATA, CAR.HYUNDAI_SANTA_FE_2022, CAR.KIA_K5_2021, CAR.HYUNDAI_ELANTRA_2021,
Ecu.engine: [CAR.HYUNDAI_AZERA_6TH_GEN, CAR.HYUNDAI_AZERA_HEV_6TH_GEN, CAR.HYUNDAI_PALISADE, CAR.HYUNDAI_SONATA], CAR.HYUNDAI_SANTA_FE, CAR.HYUNDAI_KONA_EV_2022, CAR.HYUNDAI_KONA_EV, CAR.HYUNDAI_CUSTIN_1ST_GEN, CAR.KIA_SORENTO,
Ecu.abs: [CAR.HYUNDAI_PALISADE, CAR.HYUNDAI_SONATA], CAR.KIA_CEED, CAR.KIA_SELTOS],
}, },
extra_ecus=[ extra_ecus=[
(Ecu.adas, 0x730, None), # ADAS Driving ECU on HDA2 platforms (Ecu.adas, 0x730, None), # ADAS Driving ECU on HDA2 platforms

@ -11,6 +11,7 @@ FW_VERSIONS = {
(Ecu.engine, 0x7e0, None): [ (Ecu.engine, 0x7e0, None): [
b'PEW5-188K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PEW5-188K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'PW67-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PW67-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'PX2D-188K2-G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'PX2G-188K2-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PX2G-188K2-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'PX2H-188K2-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PX2H-188K2-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'PX2H-188K2-J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PX2H-188K2-J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
@ -38,6 +39,7 @@ FW_VERSIONS = {
b'PXFG-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PXFG-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'PYB2-21PS1-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PYB2-21PS1-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'PYB2-21PS1-J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PYB2-21PS1-J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'PYJ3-21PS1-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'SH51-21PS1-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'SH51-21PS1-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
], ],
}, },
@ -252,6 +254,7 @@ FW_VERSIONS = {
b'GSH7-67XK2-P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GSH7-67XK2-P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'GSH7-67XK2-S\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GSH7-67XK2-S\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'GSH7-67XK2-T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GSH7-67XK2-T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'GSH7-67XK2-U\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.transmission, 0x7e1, None): [ (Ecu.transmission, 0x7e1, None): [
b'PXM4-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PXM4-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',

@ -263,15 +263,15 @@ class TestFwFingerprintTiming(unittest.TestCase):
print(f'get_vin {name} case, query time={self.total_time / self.N} seconds') print(f'get_vin {name} case, query time={self.total_time / self.N} seconds')
def test_fw_query_timing(self): def test_fw_query_timing(self):
total_ref_time = {1: 8.6, 2: 9.5} total_ref_time = {1: 8.1, 2: 8.7}
brand_ref_times = { brand_ref_times = {
1: { 1: {
'gm': 1.0, 'gm': 1.0,
'body': 0.1, 'body': 0.1,
'chrysler': 0.3, 'chrysler': 0.3,
'ford': 1.5, 'ford': 1.5,
'honda': 0.55, 'honda': 0.45,
'hyundai': 1.05, 'hyundai': 0.65,
'mazda': 0.1, 'mazda': 0.1,
'nissan': 0.8, 'nissan': 0.8,
'subaru': 0.65, 'subaru': 0.65,
@ -281,7 +281,7 @@ class TestFwFingerprintTiming(unittest.TestCase):
}, },
2: { 2: {
'ford': 1.6, 'ford': 1.6,
'hyundai': 1.85, 'hyundai': 1.15,
'tesla': 0.3, 'tesla': 0.3,
} }
} }

@ -15,7 +15,7 @@ from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.realtime import DT_CTRL from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.car import gen_empty_fingerprint from openpilot.selfdrive.car import gen_empty_fingerprint
from openpilot.selfdrive.car.fingerprints import all_known_cars from openpilot.selfdrive.car.fingerprints import all_known_cars, MIGRATION
from openpilot.selfdrive.car.car_helpers import FRAME_FINGERPRINT, interfaces from openpilot.selfdrive.car.car_helpers import FRAME_FINGERPRINT, interfaces
from openpilot.selfdrive.car.honda.values import CAR as HONDA, HondaFlags from openpilot.selfdrive.car.honda.values import CAR as HONDA, HondaFlags
from openpilot.selfdrive.car.tests.routes import non_tested_cars, routes, CarTestRoute from openpilot.selfdrive.car.tests.routes import non_tested_cars, routes, CarTestRoute
@ -95,7 +95,8 @@ class TestCarModelBase(unittest.TestCase):
if msg.carParams.openpilotLongitudinalControl: if msg.carParams.openpilotLongitudinalControl:
experimental_long = True experimental_long = True
if cls.platform is None and not cls.ci: if cls.platform is None and not cls.ci:
cls.platform = msg.carParams.carFingerprint live_fingerprint = msg.carParams.carFingerprint
cls.platform = MIGRATION.get(live_fingerprint, live_fingerprint)
# Log which can frame the panda safety mode left ELM327, for CAN validity checks # Log which can frame the panda safety mode left ELM327, for CAN validity checks
elif msg.which() == 'pandaStates': elif msg.which() == 'pandaStates':

@ -377,6 +377,7 @@ FW_VERSIONS = {
(Ecu.fwdRadar, 0x750, 0xf): [ (Ecu.fwdRadar, 0x750, 0xf): [
b'\x018821FF410200\x00\x00\x00\x00', b'\x018821FF410200\x00\x00\x00\x00',
b'\x018821FF410300\x00\x00\x00\x00', b'\x018821FF410300\x00\x00\x00\x00',
b'\x018821FF410400\x00\x00\x00\x00',
b'\x018821FF410500\x00\x00\x00\x00', b'\x018821FF410500\x00\x00\x00\x00',
], ],
(Ecu.fwdCamera, 0x750, 0x6d): [ (Ecu.fwdCamera, 0x750, 0x6d): [
@ -727,6 +728,7 @@ FW_VERSIONS = {
b'\x018966353M7000\x00\x00\x00\x00', b'\x018966353M7000\x00\x00\x00\x00',
b'\x018966353M7100\x00\x00\x00\x00', b'\x018966353M7100\x00\x00\x00\x00',
b'\x018966353Q2000\x00\x00\x00\x00', b'\x018966353Q2000\x00\x00\x00\x00',
b'\x018966353Q2100\x00\x00\x00\x00',
b'\x018966353Q2300\x00\x00\x00\x00', b'\x018966353Q2300\x00\x00\x00\x00',
b'\x018966353Q4000\x00\x00\x00\x00', b'\x018966353Q4000\x00\x00\x00\x00',
b'\x018966353R1100\x00\x00\x00\x00', b'\x018966353R1100\x00\x00\x00\x00',
@ -1417,6 +1419,7 @@ FW_VERSIONS = {
b'\x018821F6201400\x00\x00\x00\x00', b'\x018821F6201400\x00\x00\x00\x00',
], ],
(Ecu.fwdCamera, 0x750, 0x6d): [ (Ecu.fwdCamera, 0x750, 0x6d): [
b'\x028646F1104200\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
b'\x028646F1105200\x00\x00\x00\x008646G3304000\x00\x00\x00\x00', b'\x028646F1105200\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
], ],
}, },

@ -1,4 +1,4 @@
from typing import Any, Callable, cast from typing import cast
from openpilot.selfdrive.car.body.values import CAR as BODY from openpilot.selfdrive.car.body.values import CAR as BODY
from openpilot.selfdrive.car.chrysler.values import CAR as CHRYSLER from openpilot.selfdrive.car.chrysler.values import CAR as CHRYSLER
from openpilot.selfdrive.car.ford.values import CAR as FORD from openpilot.selfdrive.car.ford.values import CAR as FORD
@ -17,9 +17,3 @@ Platform = BODY | CHRYSLER | FORD | GM | HONDA | HYUNDAI | MAZDA | MOCK | NISSAN
BRANDS = [BODY, CHRYSLER, FORD, GM, HONDA, HYUNDAI, MAZDA, MOCK, NISSAN, SUBARU, TESLA, TOYOTA, VOLKSWAGEN] BRANDS = [BODY, CHRYSLER, FORD, GM, HONDA, HYUNDAI, MAZDA, MOCK, NISSAN, SUBARU, TESLA, TOYOTA, VOLKSWAGEN]
PLATFORMS: dict[str, Platform] = {str(platform): platform for brand in BRANDS for platform in cast(list[Platform], brand)} PLATFORMS: dict[str, Platform] = {str(platform): platform for brand in BRANDS for platform in cast(list[Platform], brand)}
MapFunc = Callable[[Platform], Any]
def create_platform_map(func: MapFunc):
return {str(platform): func(platform) for platform in PLATFORMS.values() if func(platform) is not None}

@ -114,21 +114,24 @@ class CarState(CarStateBase):
# Update ACC radar status. # Update ACC radar status.
self.acc_type = ext_cp.vl["ACC_06"]["ACC_Typ"] self.acc_type = ext_cp.vl["ACC_06"]["ACC_Typ"]
if pt_cp.vl["TSK_06"]["TSK_Status"] == 2:
# ACC okay and enabled, but not currently engaged # ACC okay but disabled (1), ACC ready (2), a radar visibility or other fault/disruption (6 or 7)
ret.cruiseState.available = True # currently regulating speed (3), driver accel override (4), brake only (5)
ret.cruiseState.enabled = False ret.cruiseState.available = pt_cp.vl["TSK_06"]["TSK_Status"] in (2, 3, 4, 5)
elif pt_cp.vl["TSK_06"]["TSK_Status"] in (3, 4, 5): ret.cruiseState.enabled = pt_cp.vl["TSK_06"]["TSK_Status"] in (3, 4, 5)
# ACC okay and enabled, currently regulating speed (3) or driver accel override (4) or brake only (5)
ret.cruiseState.available = True if self.CP.pcmCruise:
ret.cruiseState.enabled = True # Cruise Control mode; check for distance UI setting from the radar.
# ECM does not manage this, so do not need to check for openpilot longitudinal
ret.cruiseState.nonAdaptive = ext_cp.vl["ACC_02"]["ACC_Gesetzte_Zeitluecke"] == 0
else: else:
# ACC okay but disabled (1), or a radar visibility or other fault/disruption (6 or 7) # Speed limiter mode; ECM faults if we command ACC while not pcmCruise
ret.cruiseState.available = False ret.cruiseState.nonAdaptive = bool(pt_cp.vl["TSK_06"]["TSK_Limiter_ausgewaehlt"])
ret.cruiseState.enabled = False
ret.accFaulted = pt_cp.vl["TSK_06"]["TSK_Status"] in (6, 7)
self.esp_hold_confirmation = bool(pt_cp.vl["ESP_21"]["ESP_Haltebestaetigung"]) self.esp_hold_confirmation = bool(pt_cp.vl["ESP_21"]["ESP_Haltebestaetigung"])
ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation
ret.accFaulted = pt_cp.vl["TSK_06"]["TSK_Status"] in (6, 7)
# Update ACC setpoint. When the setpoint is zero or there's an error, the # Update ACC setpoint. When the setpoint is zero or there's an error, the
# radar sends a set-speed of ~90.69 m/s / 203mph. # radar sends a set-speed of ~90.69 m/s / 203mph.

@ -386,6 +386,7 @@ FW_VERSIONS = {
b'\xf1\x8704L906026ET\xf1\x891990', b'\xf1\x8704L906026ET\xf1\x891990',
b'\xf1\x8704L906026FP\xf1\x892012', b'\xf1\x8704L906026FP\xf1\x892012',
b'\xf1\x8704L906026GA\xf1\x892013', b'\xf1\x8704L906026GA\xf1\x892013',
b'\xf1\x8704L906026GK\xf1\x899971',
b'\xf1\x8704L906026KD\xf1\x894798', b'\xf1\x8704L906026KD\xf1\x894798',
b'\xf1\x8705L906022A \xf1\x890827', b'\xf1\x8705L906022A \xf1\x890827',
b'\xf1\x873G0906259 \xf1\x890004', b'\xf1\x873G0906259 \xf1\x890004',
@ -393,6 +394,7 @@ FW_VERSIONS = {
b'\xf1\x873G0906264 \xf1\x890004', b'\xf1\x873G0906264 \xf1\x890004',
], ],
(Ecu.transmission, 0x7e1, None): [ (Ecu.transmission, 0x7e1, None): [
b'\xf1\x870CW300041E \xf1\x891006',
b'\xf1\x870CW300042H \xf1\x891601', b'\xf1\x870CW300042H \xf1\x891601',
b'\xf1\x870CW300042H \xf1\x891607', b'\xf1\x870CW300042H \xf1\x891607',
b'\xf1\x870CW300043H \xf1\x891601', b'\xf1\x870CW300043H \xf1\x891601',
@ -432,6 +434,7 @@ FW_VERSIONS = {
b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522B0060803', b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522B0060803',
b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522B0080803', b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522B0080803',
b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820526B0060905', b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820526B0060905',
b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820531B0062105',
b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521B00606A1', b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521B00606A1',
b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516B00501A1', b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516B00501A1',
b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521B00603A1', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521B00603A1',
@ -1068,6 +1071,7 @@ FW_VERSIONS = {
b'\xf1\x8704L906021ER\xf1\x898361', b'\xf1\x8704L906021ER\xf1\x898361',
b'\xf1\x8704L906026BP\xf1\x897608', b'\xf1\x8704L906026BP\xf1\x897608',
b'\xf1\x8704L906026BS\xf1\x891541', b'\xf1\x8704L906026BS\xf1\x891541',
b'\xf1\x8704L906026BT\xf1\x897612',
b'\xf1\x875G0906259C \xf1\x890002', b'\xf1\x875G0906259C \xf1\x890002',
], ],
(Ecu.transmission, 0x7e1, None): [ (Ecu.transmission, 0x7e1, None): [
@ -1076,6 +1080,7 @@ FW_VERSIONS = {
b'\xf1\x870CW300043B \xf1\x891601', b'\xf1\x870CW300043B \xf1\x891601',
b'\xf1\x870CW300043P \xf1\x891605', b'\xf1\x870CW300043P \xf1\x891605',
b'\xf1\x870D9300012H \xf1\x894518', b'\xf1\x870D9300012H \xf1\x894518',
b'\xf1\x870D9300014T \xf1\x895221',
b'\xf1\x870D9300041C \xf1\x894936', b'\xf1\x870D9300041C \xf1\x894936',
b'\xf1\x870D9300041H \xf1\x895220', b'\xf1\x870D9300041H \xf1\x895220',
b'\xf1\x870D9300041J \xf1\x894902', b'\xf1\x870D9300041J \xf1\x894902',
@ -1096,6 +1101,7 @@ FW_VERSIONS = {
b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566A01513A1', b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566A01513A1',
b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521T00403A1', b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521T00403A1',
b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521T00403A1', b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521T00403A1',
b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521T00603A1',
b'\xf1\x875Q0909144R \xf1\x891061\xf1\x82\x0516A00604A1', b'\xf1\x875Q0909144R \xf1\x891061\xf1\x82\x0516A00604A1',
b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521T00601A1', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521T00601A1',
b'\xf1\x875QD909144E \xf1\x891081\xf1\x82\x0521T00503A1', b'\xf1\x875QD909144E \xf1\x891081\xf1\x82\x0521T00503A1',

@ -334,6 +334,7 @@ class CAR(Platforms):
[ [
VWCarDocs("Škoda Octavia 2015-19"), VWCarDocs("Škoda Octavia 2015-19"),
VWCarDocs("Škoda Octavia RS 2016"), VWCarDocs("Škoda Octavia RS 2016"),
VWCarDocs("Škoda Octavia Scout 2017-19"),
], ],
VolkswagenCarSpecs(mass=1388, wheelbase=2.68), VolkswagenCarSpecs(mass=1388, wheelbase=2.68),
) )
@ -370,7 +371,7 @@ FW_QUERY_CONFIG = FwQueryConfig(
Request( Request(
[VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_REQUEST_MULTI],
[VOLKSWAGEN_VERSION_RESPONSE], [VOLKSWAGEN_VERSION_RESPONSE],
# whitelist_ecus=[Ecu.srs, Ecu.eps, Ecu.fwdRadar], whitelist_ecus=[Ecu.srs, Ecu.eps, Ecu.fwdRadar, Ecu.fwdCamera],
rx_offset=VOLKSWAGEN_RX_OFFSET, rx_offset=VOLKSWAGEN_RX_OFFSET,
bus=bus, bus=bus,
logging=(bus != 1 or not obd_multiplexing), logging=(bus != 1 or not obd_multiplexing),
@ -379,7 +380,7 @@ FW_QUERY_CONFIG = FwQueryConfig(
Request( Request(
[VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_REQUEST_MULTI],
[VOLKSWAGEN_VERSION_RESPONSE], [VOLKSWAGEN_VERSION_RESPONSE],
# whitelist_ecus=[Ecu.engine, Ecu.transmission], whitelist_ecus=[Ecu.engine, Ecu.transmission],
bus=bus, bus=bus,
logging=(bus != 1 or not obd_multiplexing), logging=(bus != 1 or not obd_multiplexing),
obd_multiplexing=obd_multiplexing, obd_multiplexing=obd_multiplexing,

@ -143,6 +143,7 @@ class Controls:
self.logged_comm_issue = None self.logged_comm_issue = None
self.not_running_prev = None self.not_running_prev = None
self.steer_limited = False self.steer_limited = False
self.last_actuators = car.CarControl.Actuators.new_message()
self.desired_curvature = 0.0 self.desired_curvature = 0.0
self.experimental_mode = False self.experimental_mode = False
self.personality = self.read_personality_param() self.personality = self.read_personality_param()
@ -620,7 +621,7 @@ class Controls:
undershooting = abs(lac_log.desiredLateralAccel) / abs(1e-3 + lac_log.actualLateralAccel) > 1.2 undershooting = abs(lac_log.desiredLateralAccel) / abs(1e-3 + lac_log.actualLateralAccel) > 1.2
turning = abs(lac_log.desiredLateralAccel) > 1.0 turning = abs(lac_log.desiredLateralAccel) > 1.0
good_speed = CS.vEgo > 5 good_speed = CS.vEgo > 5
max_torque = abs(actuators.steer) > 0.99 max_torque = abs(self.last_actuators.steer) > 0.99
if undershooting and turning and good_speed and max_torque: if undershooting and turning and good_speed and max_torque:
lac_log.active and self.events.add(EventName.steerSaturated) lac_log.active and self.events.add(EventName.steerSaturated)
elif lac_log.saturated: elif lac_log.saturated:
@ -727,6 +728,7 @@ class Controls:
if not self.CP.passive and self.initialized: if not self.CP.passive and self.initialized:
self.card.controls_update(CC) self.card.controls_update(CC)
self.last_actuators = CO.actuatorsOutput
if self.CP.steerControlType == car.CarParams.SteerControlType.angle: if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
self.steer_limited = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \ self.steer_limited = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \
STEER_ANGLE_SATURATION_THRESHOLD STEER_ANGLE_SATURATION_THRESHOLD

@ -25,11 +25,12 @@ if __name__ == '__main__':
CC = car.CarControl.new_message() CC = car.CarControl.new_message()
ets = [] ets = []
for _ in tqdm(range(N_RUNS)): for _ in tqdm(range(N_RUNS)):
msgs = [(m.as_builder().to_bytes(),) for m in tm.can_msgs]
start_t = time.process_time_ns() start_t = time.process_time_ns()
for msg in tm.can_msgs: for msg in msgs:
for cp in tm.CI.can_parsers: for cp in tm.CI.can_parsers:
if cp is not None: if cp is not None:
cp.update_strings((msg.as_builder().to_bytes(),)) cp.update_strings(msg)
ets.append((time.process_time_ns() - start_t) * 1e-6) ets.append((time.process_time_ns() - start_t) * 1e-6)
print(f'{len(tm.can_msgs)} CAN packets, {N_RUNS} runs') print(f'{len(tm.can_msgs)} CAN packets, {N_RUNS} runs')

@ -52,7 +52,7 @@ def cycle_alerts(duration=200, is_metric=False):
cameras = ['roadCameraState', 'wideRoadCameraState', 'driverCameraState'] cameras = ['roadCameraState', 'wideRoadCameraState', 'driverCameraState']
CS = car.CarState.new_message() CS = car.CarState.new_message()
CP = CarInterface.get_non_essential_params("CIVIC") CP = CarInterface.get_non_essential_params("HONDA_CIVIC")
sm = messaging.SubMaster(['deviceState', 'pandaStates', 'roadCameraState', 'modelV2', 'liveCalibration', sm = messaging.SubMaster(['deviceState', 'pandaStates', 'roadCameraState', 'modelV2', 'liveCalibration',
'driverMonitoringState', 'longitudinalPlan', 'liveLocationKalman', 'driverMonitoringState', 'longitudinalPlan', 'liveLocationKalman',
'managerState'] + cameras) 'managerState'] + cameras)

@ -9,7 +9,7 @@ from openpilot.common.spinner import Spinner
from openpilot.common.text_window import TextWindow from openpilot.common.text_window import TextWindow
from openpilot.system.hardware import AGNOS from openpilot.system.hardware import AGNOS
from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.common.swaglog import cloudlog, add_file_handler
from openpilot.system.version import is_dirty from openpilot.system.version import get_build_metadata
MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9 MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9
CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache") CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache")
@ -86,4 +86,5 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None:
if __name__ == "__main__": if __name__ == "__main__":
spinner = Spinner() spinner = Spinner()
spinner.update_progress(0, 100) spinner.update_progress(0, 100)
build(spinner, is_dirty(), minimal = AGNOS) build_metadata = get_build_metadata()
build(spinner, build_metadata.openpilot.is_dirty, minimal = AGNOS)

@ -16,21 +16,20 @@ from openpilot.selfdrive.manager.process import ensure_running
from openpilot.selfdrive.manager.process_config import managed_processes from openpilot.selfdrive.manager.process_config import managed_processes
from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID
from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.common.swaglog import cloudlog, add_file_handler
from openpilot.common.git import get_commit, get_origin, get_short_branch, get_commit_date from openpilot.system.version import get_build_metadata, terms_version, training_version
from openpilot.system.version import is_dirty, get_version, \
get_normalized_origin, terms_version, training_version, \
is_tested_branch, is_release_branch
def manager_init() -> None: def manager_init() -> None:
save_bootlog() save_bootlog()
build_metadata = get_build_metadata()
params = Params() params = Params()
params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START) params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START)
params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION)
params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION)
if is_release_branch(): if build_metadata.release_channel:
params.clear_all(ParamKeyType.DEVELOPMENT_ONLY) params.clear_all(ParamKeyType.DEVELOPMENT_ONLY)
default_params: list[tuple[str, str | bytes]] = [ default_params: list[tuple[str, str | bytes]] = [
@ -62,15 +61,15 @@ def manager_init() -> None:
print("WARNING: failed to make /dev/shm") print("WARNING: failed to make /dev/shm")
# set version params # set version params
params.put("Version", get_version()) params.put("Version", build_metadata.openpilot.version)
params.put("TermsVersion", terms_version) params.put("TermsVersion", terms_version)
params.put("TrainingVersion", training_version) params.put("TrainingVersion", training_version)
params.put("GitCommit", get_commit()) params.put("GitCommit", build_metadata.openpilot.git_commit)
params.put("GitCommitDate", get_commit_date()) params.put("GitCommitDate", build_metadata.openpilot.git_commit_date)
params.put("GitBranch", get_short_branch()) params.put("GitBranch", build_metadata.channel)
params.put("GitRemote", get_origin()) params.put("GitRemote", build_metadata.openpilot.git_origin)
params.put_bool("IsTestedBranch", is_tested_branch()) params.put_bool("IsTestedBranch", build_metadata.tested_channel)
params.put_bool("IsReleaseBranch", is_release_branch()) params.put_bool("IsReleaseBranch", build_metadata.release_channel)
# set dongle id # set dongle id
reg_res = register(show_spinner=True) reg_res = register(show_spinner=True)
@ -80,21 +79,21 @@ def manager_init() -> None:
serial = params.get("HardwareSerial") serial = params.get("HardwareSerial")
raise Exception(f"Registration failed for device {serial}") raise Exception(f"Registration failed for device {serial}")
os.environ['DONGLE_ID'] = dongle_id # Needed for swaglog os.environ['DONGLE_ID'] = dongle_id # Needed for swaglog
os.environ['GIT_ORIGIN'] = get_normalized_origin() # Needed for swaglog os.environ['GIT_ORIGIN'] = build_metadata.openpilot.git_normalized_origin # Needed for swaglog
os.environ['GIT_BRANCH'] = get_short_branch() # Needed for swaglog os.environ['GIT_BRANCH'] = build_metadata.channel # Needed for swaglog
os.environ['GIT_COMMIT'] = get_commit() # Needed for swaglog os.environ['GIT_COMMIT'] = build_metadata.openpilot.git_commit # Needed for swaglog
if not is_dirty(): if not build_metadata.openpilot.is_dirty:
os.environ['CLEAN'] = '1' os.environ['CLEAN'] = '1'
# init logging # init logging
sentry.init(sentry.SentryProject.SELFDRIVE) sentry.init(sentry.SentryProject.SELFDRIVE)
cloudlog.bind_global(dongle_id=dongle_id, cloudlog.bind_global(dongle_id=dongle_id,
version=get_version(), version=build_metadata.openpilot.version,
origin=get_normalized_origin(), origin=build_metadata.openpilot.git_normalized_origin,
branch=get_short_branch(), branch=build_metadata.channel,
commit=get_commit(), commit=build_metadata.openpilot.git_commit,
dirty=is_dirty(), dirty=build_metadata.openpilot.is_dirty,
device=HARDWARE.get_device_type()) device=HARDWARE.get_device_type())
# preimport all processes # preimport all processes

@ -48,15 +48,14 @@ class RouteEngine:
self.reroute_counter = 0 self.reroute_counter = 0
self.api = None
self.mapbox_token = None
if "MAPBOX_TOKEN" in os.environ: if "MAPBOX_TOKEN" in os.environ:
self.mapbox_token = os.environ["MAPBOX_TOKEN"] self.mapbox_token = os.environ["MAPBOX_TOKEN"]
self.mapbox_host = "https://api.mapbox.com" self.mapbox_host = "https://api.mapbox.com"
else: else:
try: self.api = Api(self.params.get("DongleId", encoding='utf8'))
self.mapbox_token = Api(self.params.get("DongleId", encoding='utf8')).get_token(expiry_hours=4 * 7 * 24)
except FileNotFoundError:
cloudlog.exception("Failed to generate mapbox token due to missing private key. Ensure device is registered.")
self.mapbox_token = ""
self.mapbox_host = "https://maps.comma.ai" self.mapbox_host = "https://maps.comma.ai"
def update(self): def update(self):
@ -122,8 +121,12 @@ class RouteEngine:
if lang is not None: if lang is not None:
lang = lang.replace('main_', '') lang = lang.replace('main_', '')
token = self.mapbox_token
if token is None:
token = self.api.get_token()
params = { params = {
'access_token': self.mapbox_token, 'access_token': token,
'annotations': 'maxspeed', 'annotations': 'maxspeed',
'geometries': 'geojson', 'geometries': 'geojson',
'overview': 'full', 'overview': 'full',

@ -6,9 +6,8 @@ from sentry_sdk.integrations.threading import ThreadingIntegration
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.selfdrive.athena.registration import is_registered_device from openpilot.selfdrive.athena.registration import is_registered_device
from openpilot.system.hardware import HARDWARE, PC from openpilot.system.hardware import HARDWARE, PC
from openpilot.common.git import get_commit, get_branch, get_origin
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.system.version import get_version, is_comma_remote, is_dirty, is_tested_branch from openpilot.system.version import get_build_metadata, get_version
class SentryProject(Enum): class SentryProject(Enum):
@ -43,12 +42,13 @@ def set_tag(key: str, value: str) -> None:
def init(project: SentryProject) -> bool: def init(project: SentryProject) -> bool:
build_metadata = get_build_metadata()
# forks like to mess with this, so double check # forks like to mess with this, so double check
comma_remote = is_comma_remote() and "commaai" in get_origin() comma_remote = build_metadata.openpilot.comma_remote and "commaai" in build_metadata.openpilot.git_origin
if not comma_remote or not is_registered_device() or PC: if not comma_remote or not is_registered_device() or PC:
return False return False
env = "release" if is_tested_branch() else "master" env = "release" if build_metadata.tested_channel else "master"
dongle_id = Params().get("DongleId", encoding='utf-8') dongle_id = Params().get("DongleId", encoding='utf-8')
integrations = [] integrations = []
@ -63,11 +63,13 @@ def init(project: SentryProject) -> bool:
max_value_length=8192, max_value_length=8192,
environment=env) environment=env)
build_metadata = get_build_metadata()
sentry_sdk.set_user({"id": dongle_id}) sentry_sdk.set_user({"id": dongle_id})
sentry_sdk.set_tag("dirty", is_dirty()) sentry_sdk.set_tag("dirty", build_metadata.openpilot.is_dirty)
sentry_sdk.set_tag("origin", get_origin()) sentry_sdk.set_tag("origin", build_metadata.openpilot.git_origin)
sentry_sdk.set_tag("branch", get_branch()) sentry_sdk.set_tag("branch", build_metadata.channel)
sentry_sdk.set_tag("commit", get_commit()) sentry_sdk.set_tag("commit", build_metadata.openpilot.git_commit)
sentry_sdk.set_tag("device", HARDWARE.get_device_type()) sentry_sdk.set_tag("device", HARDWARE.get_device_type())
if project == SentryProject.SELFDRIVE: if project == SentryProject.SELFDRIVE:

@ -13,7 +13,7 @@ from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import HARDWARE from openpilot.system.hardware import HARDWARE
from openpilot.common.file_helpers import atomic_write_in_dir from openpilot.common.file_helpers import atomic_write_in_dir
from openpilot.system.version import get_normalized_origin, get_short_branch, get_short_version, is_dirty from openpilot.system.version import get_build_metadata
from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S
@ -86,13 +86,15 @@ def main() -> NoReturn:
# initialize stats directory # initialize stats directory
Path(STATS_DIR).mkdir(parents=True, exist_ok=True) Path(STATS_DIR).mkdir(parents=True, exist_ok=True)
build_metadata = get_build_metadata()
# initialize tags # initialize tags
tags = { tags = {
'started': False, 'started': False,
'version': get_short_version(), 'version': build_metadata.openpilot.version,
'branch': get_short_branch(), 'branch': build_metadata.channel,
'dirty': is_dirty(), 'dirty': build_metadata.openpilot.is_dirty,
'origin': get_normalized_origin(), 'origin': build_metadata.openpilot.git_normalized_origin,
'deviceType': HARDWARE.get_device_type(), 'deviceType': HARDWARE.get_device_type(),
} }

@ -1 +1 @@
4e5e37be9d70450154f8b100ed88151bb3612331 28001018eae89eb4906717bebf892345ad590b5a

@ -207,17 +207,10 @@ def thermald_thread(end_event, hw_queue) -> None:
while not end_event.is_set(): while not end_event.is_set():
sm.update(PANDA_STATES_TIMEOUT) sm.update(PANDA_STATES_TIMEOUT)
# Run at 2Hz
if sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_TRML) != 0:
continue
pandaStates = sm['pandaStates'] pandaStates = sm['pandaStates']
peripheralState = sm['peripheralState'] peripheralState = sm['peripheralState']
peripheral_panda_present = peripheralState.pandaType != log.PandaState.PandaType.unknown peripheral_panda_present = peripheralState.pandaType != log.PandaState.PandaType.unknown
msg = read_thermal(thermal_config)
msg.deviceState.deviceType = HARDWARE.get_device_type()
if sm.updated['pandaStates'] and len(pandaStates) > 0: if sm.updated['pandaStates'] and len(pandaStates) > 0:
# Set ignition based on any panda connected # Set ignition based on any panda connected
@ -237,6 +230,14 @@ def thermald_thread(end_event, hw_queue) -> None:
onroad_conditions["ignition"] = False onroad_conditions["ignition"] = False
cloudlog.error("panda timed out onroad") cloudlog.error("panda timed out onroad")
# Run at 2Hz, plus rising edge of ignition
ign_edge = started_ts is None and onroad_conditions["ignition"]
if (sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_TRML) != 0) and not ign_edge:
continue
msg = read_thermal(thermal_config)
msg.deviceState.deviceType = HARDWARE.get_device_type()
try: try:
last_hw_state = hw_queue.get_nowait() last_hw_state = hw_queue.get_nowait()
except queue.Empty: except queue.Empty:

@ -11,8 +11,8 @@ from typing import NoReturn
import openpilot.selfdrive.sentry as sentry import openpilot.selfdrive.sentry as sentry
from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import Paths
from openpilot.common.git import get_commit
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.system.version import get_build_metadata
MAX_SIZE = 1_000_000 * 100 # allow up to 100M MAX_SIZE = 1_000_000 * 100 # allow up to 100M
MAX_TOMBSTONE_FN_LEN = 62 # 85 - 23 ("<dongle id>/crash/") MAX_TOMBSTONE_FN_LEN = 62 # 85 - 23 ("<dongle id>/crash/")
@ -124,7 +124,9 @@ def report_tombstone_apport(fn):
clean_path = path.replace('/', '_') clean_path = path.replace('/', '_')
date = datetime.datetime.now().strftime("%Y-%m-%d--%H-%M-%S") date = datetime.datetime.now().strftime("%Y-%m-%d--%H-%M-%S")
new_fn = f"{date}_{(get_commit() or 'nocommit')[:8]}_{safe_fn(clean_path)}"[:MAX_TOMBSTONE_FN_LEN] build_metadata = get_build_metadata()
new_fn = f"{date}_{(build_metadata.openpilot.git_commit or 'nocommit')[:8]}_{safe_fn(clean_path)}"[:MAX_TOMBSTONE_FN_LEN]
crashlog_dir = os.path.join(Paths.log_root(), "crash") crashlog_dir = os.path.join(Paths.log_root(), "crash")
os.makedirs(crashlog_dir, exist_ok=True) os.makedirs(crashlog_dir, exist_ok=True)

@ -137,17 +137,17 @@ std::optional<QMapLibre::Coordinate> coordinate_from_param(const std::string &pa
// return {distance, unit} // return {distance, unit}
std::pair<QString, QString> map_format_distance(float d, bool is_metric) { std::pair<QString, QString> map_format_distance(float d, bool is_metric) {
auto round_distance = [](float d) -> float { auto round_distance = [](float d) -> QString {
return (d > 10) ? std::nearbyint(d) : std::nearbyint(d * 10) / 10.0; return (d > 10) ? QString::number(std::nearbyint(d)) : QString::number(std::nearbyint(d * 10) / 10.0, 'f', 1);
}; };
d = std::max(d, 0.0f); d = std::max(d, 0.0f);
if (is_metric) { if (is_metric) {
return (d > 500) ? std::pair{QString::number(round_distance(d / 1000)), QObject::tr("km")} return (d > 500) ? std::pair{round_distance(d / 1000), QObject::tr("km")}
: std::pair{QString::number(50 * std::nearbyint(d / 50)), QObject::tr("m")}; : std::pair{QString::number(50 * std::nearbyint(d / 50)), QObject::tr("m")};
} else { } else {
float feet = d * METER_TO_FOOT; float feet = d * METER_TO_FOOT;
return (feet > 500) ? std::pair{QString::number(round_distance(d * METER_TO_MILE)), QObject::tr("mi")} return (feet > 500) ? std::pair{round_distance(d * METER_TO_MILE), QObject::tr("mi")}
: std::pair{QString::number(50 * std::nearbyint(d / 50)), QObject::tr("ft")}; : std::pair{QString::number(50 * std::nearbyint(d / 50)), QObject::tr("ft")};
} }
} }

@ -1,5 +1,3 @@
#include "selfdrive/ui/qt/offroad/settings.h"
#include <cassert> #include <cassert>
#include <cmath> #include <cmath>
#include <string> #include <string>
@ -8,20 +6,14 @@
#include <QDebug> #include <QDebug>
#include "selfdrive/ui/qt/network/networking.h"
#include "common/params.h"
#include "common/watchdog.h" #include "common/watchdog.h"
#include "common/util.h" #include "common/util.h"
#include "system/hardware/hw.h" #include "selfdrive/ui/qt/network/networking.h"
#include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/offroad/settings.h"
#include "selfdrive/ui/qt/widgets/input.h" #include "selfdrive/ui/qt/qt_window.h"
#include "selfdrive/ui/qt/widgets/prime.h"
#include "selfdrive/ui/qt/widgets/scrollview.h" #include "selfdrive/ui/qt/widgets/scrollview.h"
#include "selfdrive/ui/qt/widgets/ssh_keys.h" #include "selfdrive/ui/qt/widgets/ssh_keys.h"
#include "selfdrive/ui/qt/widgets/toggle.h"
#include "selfdrive/ui/ui.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/qt_window.h"
TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
// param, title, desc, icon // param, title, desc, icon
@ -215,6 +207,14 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
addItem(new LabelControl(tr("Dongle ID"), getDongleId().value_or(tr("N/A")))); addItem(new LabelControl(tr("Dongle ID"), getDongleId().value_or(tr("N/A"))));
addItem(new LabelControl(tr("Serial"), params.get("HardwareSerial").c_str())); addItem(new LabelControl(tr("Serial"), params.get("HardwareSerial").c_str()));
pair_device = new ButtonControl(tr("Pair Device"), tr("PAIR"),
tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."));
connect(pair_device, &ButtonControl::clicked, [=]() {
PairingPopup popup(this);
popup.exec();
});
addItem(pair_device);
// offroad-only buttons // offroad-only buttons
auto dcamBtn = new ButtonControl(tr("Driver Camera"), tr("PREVIEW"), auto dcamBtn = new ButtonControl(tr("Driver Camera"), tr("PREVIEW"),
@ -262,9 +262,14 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
}); });
addItem(translateBtn); addItem(translateBtn);
QObject::connect(uiState(), &UIState::primeChanged, [this] (bool prime) {
pair_device->setVisible(!prime);
});
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
for (auto btn : findChildren<ButtonControl *>()) { for (auto btn : findChildren<ButtonControl *>()) {
btn->setEnabled(offroad); if (btn != pair_device) {
btn->setEnabled(offroad);
}
} }
}); });
@ -345,6 +350,11 @@ void DevicePanel::poweroff() {
} }
} }
void DevicePanel::showEvent(QShowEvent *event) {
pair_device->setVisible(!uiState()->primeType());
ListWidget::showEvent(event);
}
void SettingsWindow::showEvent(QShowEvent *event) { void SettingsWindow::showEvent(QShowEvent *event) {
setCurrentPanel(0); setCurrentPanel(0);
} }

@ -10,7 +10,6 @@
#include <QStackedWidget> #include <QStackedWidget>
#include <QWidget> #include <QWidget>
#include "selfdrive/ui/ui.h" #include "selfdrive/ui/ui.h"
#include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/widgets/controls.h"
@ -43,6 +42,8 @@ class DevicePanel : public ListWidget {
Q_OBJECT Q_OBJECT
public: public:
explicit DevicePanel(SettingsWindow *parent); explicit DevicePanel(SettingsWindow *parent);
void showEvent(QShowEvent *event) override;
signals: signals:
void reviewTrainingGuide(); void reviewTrainingGuide();
void showDriverView(); void showDriverView();
@ -54,6 +55,7 @@ private slots:
private: private:
Params params; Params params;
ButtonControl *pair_device;
}; };
class TogglesPanel : public ListWidget { class TogglesPanel : public ListWidget {

@ -73,18 +73,16 @@ void OnroadWindow::updateState(const UIState &s) {
return; return;
} }
QColor bgColor = bg_colors[s.status];
Alert alert = Alert::get(*(s.sm), s.scene.started_frame);
alerts->updateAlert(alert);
if (s.scene.map_on_left) { if (s.scene.map_on_left) {
split->setDirection(QBoxLayout::LeftToRight); split->setDirection(QBoxLayout::LeftToRight);
} else { } else {
split->setDirection(QBoxLayout::RightToLeft); split->setDirection(QBoxLayout::RightToLeft);
} }
alerts->updateState(s);
nvg->updateState(s); nvg->updateState(s);
QColor bgColor = bg_colors[s.status];
if (bg != bgColor) { if (bg != bgColor) {
// repaint border // repaint border
bg = bgColor; bg = bgColor;
@ -105,27 +103,30 @@ void OnroadWindow::mousePressEvent(QMouseEvent* e) {
QWidget::mousePressEvent(e); QWidget::mousePressEvent(e);
} }
void OnroadWindow::createMapWidget() {
#ifdef ENABLE_MAPS
auto m = new MapPanel(get_mapbox_settings());
map = m;
QObject::connect(m, &MapPanel::mapPanelRequested, this, &OnroadWindow::mapPanelRequested);
QObject::connect(nvg->map_settings_btn, &MapSettingsButton::clicked, m, &MapPanel::toggleMapSettings);
nvg->map_settings_btn->setEnabled(true);
m->setFixedWidth(topWidget(this)->width() / 2 - UI_BORDER_SIZE);
split->insertWidget(0, m);
// hidden by default, made visible when navRoute is published
m->setVisible(false);
#endif
}
void OnroadWindow::offroadTransition(bool offroad) { void OnroadWindow::offroadTransition(bool offroad) {
#ifdef ENABLE_MAPS #ifdef ENABLE_MAPS
if (!offroad) { if (!offroad) {
if (map == nullptr && (uiState()->hasPrime() || !MAPBOX_TOKEN.isEmpty())) { if (map == nullptr && (uiState()->hasPrime() || !MAPBOX_TOKEN.isEmpty())) {
auto m = new MapPanel(get_mapbox_settings()); createMapWidget();
map = m;
QObject::connect(m, &MapPanel::mapPanelRequested, this, &OnroadWindow::mapPanelRequested);
QObject::connect(nvg->map_settings_btn, &MapSettingsButton::clicked, m, &MapPanel::toggleMapSettings);
nvg->map_settings_btn->setEnabled(true);
m->setFixedWidth(topWidget(this)->width() / 2 - UI_BORDER_SIZE);
split->insertWidget(0, m);
// hidden by default, made visible when navRoute is published
m->setVisible(false);
} }
} }
#endif #endif
alerts->clear();
alerts->updateAlert({});
} }
void OnroadWindow::primeChanged(bool prime) { void OnroadWindow::primeChanged(bool prime) {
@ -135,6 +136,8 @@ void OnroadWindow::primeChanged(bool prime) {
nvg->map_settings_btn->setVisible(false); nvg->map_settings_btn->setVisible(false);
map->deleteLater(); map->deleteLater();
map = nullptr; map = nullptr;
} else if (!map && (prime || !MAPBOX_TOKEN.isEmpty())) {
createMapWidget();
} }
#endif #endif
} }
@ -147,13 +150,56 @@ void OnroadWindow::paintEvent(QPaintEvent *event) {
// ***** onroad widgets ***** // ***** onroad widgets *****
// OnroadAlerts // OnroadAlerts
void OnroadAlerts::updateAlert(const Alert &a) {
void OnroadAlerts::updateState(const UIState &s) {
Alert a = getAlert(*(s.sm), s.scene.started_frame);
if (!alert.equal(a)) { if (!alert.equal(a)) {
alert = a; alert = a;
update(); update();
} }
} }
void OnroadAlerts::clear() {
alert = {};
update();
}
OnroadAlerts::Alert OnroadAlerts::getAlert(const SubMaster &sm, uint64_t started_frame) {
const cereal::ControlsState::Reader &cs = sm["controlsState"].getControlsState();
const uint64_t controls_frame = sm.rcv_frame("controlsState");
Alert a = {};
if (controls_frame >= started_frame) { // Don't get old alert.
a = {cs.getAlertText1().cStr(), cs.getAlertText2().cStr(),
cs.getAlertType().cStr(), cs.getAlertSize(), cs.getAlertStatus()};
}
if (!sm.updated("controlsState") && (sm.frame - started_frame) > 5 * UI_FREQ) {
const int CONTROLS_TIMEOUT = 5;
const int controls_missing = (nanos_since_boot() - sm.rcv_time("controlsState")) / 1e9;
// Handle controls timeout
if (controls_frame < started_frame) {
// car is started, but controlsState hasn't been seen at all
a = {tr("openpilot Unavailable"), tr("Waiting for controls to start"),
"controlsWaiting", cereal::ControlsState::AlertSize::MID,
cereal::ControlsState::AlertStatus::NORMAL};
} else if (controls_missing > CONTROLS_TIMEOUT && !Hardware::PC()) {
// car is started, but controls is lagging or died
if (cs.getEnabled() && (controls_missing - CONTROLS_TIMEOUT) < 10) {
a = {tr("TAKE CONTROL IMMEDIATELY"), tr("Controls Unresponsive"),
"controlsUnresponsive", cereal::ControlsState::AlertSize::FULL,
cereal::ControlsState::AlertStatus::CRITICAL};
} else {
a = {tr("Controls Unresponsive"), tr("Reboot Device"),
"controlsUnresponsivePermanent", cereal::ControlsState::AlertSize::MID,
cereal::ControlsState::AlertStatus::NORMAL};
}
}
}
return a;
}
void OnroadAlerts::paintEvent(QPaintEvent *event) { void OnroadAlerts::paintEvent(QPaintEvent *event) {
if (alert.size == cereal::ControlsState::AlertSize::NONE) { if (alert.size == cereal::ControlsState::AlertSize::NONE) {
return; return;

@ -21,12 +21,31 @@ class OnroadAlerts : public QWidget {
public: public:
OnroadAlerts(QWidget *parent = 0) : QWidget(parent) {} OnroadAlerts(QWidget *parent = 0) : QWidget(parent) {}
void updateAlert(const Alert &a); void updateState(const UIState &s);
void clear();
protected: protected:
struct Alert {
QString text1;
QString text2;
QString type;
cereal::ControlsState::AlertSize size;
cereal::ControlsState::AlertStatus status;
bool equal(const Alert &other) const {
return text1 == other.text1 && other.text2 == other.text2 && type == other.type;
}
};
const QMap<cereal::ControlsState::AlertStatus, QColor> alert_colors = {
{cereal::ControlsState::AlertStatus::NORMAL, QColor(0x15, 0x15, 0x15, 0xf1)},
{cereal::ControlsState::AlertStatus::USER_PROMPT, QColor(0xDA, 0x6F, 0x25, 0xf1)},
{cereal::ControlsState::AlertStatus::CRITICAL, QColor(0xC9, 0x22, 0x31, 0xf1)},
};
void paintEvent(QPaintEvent*) override; void paintEvent(QPaintEvent*) override;
OnroadAlerts::Alert getAlert(const SubMaster &sm, uint64_t started_frame);
private:
QColor bg; QColor bg;
Alert alert = {}; Alert alert = {};
}; };
@ -127,6 +146,7 @@ signals:
void mapPanelRequested(); void mapPanelRequested();
private: private:
void createMapWidget();
void paintEvent(QPaintEvent *event); void paintEvent(QPaintEvent *event);
void mousePressEvent(QMouseEvent* e) override; void mousePressEvent(QMouseEvent* e) override;
OnroadAlerts *alerts; OnroadAlerts *alerts;

@ -55,7 +55,7 @@ void Sidebar::mouseReleaseEvent(QMouseEvent *event) {
flag_pressed = settings_pressed = false; flag_pressed = settings_pressed = false;
update(); update();
} }
if (home_btn.contains(event->pos())) { if (onroad && home_btn.contains(event->pos())) {
MessageBuilder msg; MessageBuilder msg;
msg.initEvent().initUserFlag(); msg.initEvent().initUserFlag();
pm->send("userFlag", msg); pm->send("userFlag", msg);

@ -61,7 +61,7 @@ QString timeAgo(const QDateTime &date) {
QString s; QString s;
if (diff < 60) { if (diff < 60) {
s = "now"; s = QObject::tr("now");
} else if (diff < 60 * 60) { } else if (diff < 60 * 60) {
int minutes = diff / 60; int minutes = diff / 60;
s = QObject::tr("%n minute(s) ago", "", minutes); s = QObject::tr("%n minute(s) ago", "", minutes);
@ -105,20 +105,21 @@ void initApp(int argc, char *argv[], bool disable_hidpi) {
std::signal(SIGINT, sigTermHandler); std::signal(SIGINT, sigTermHandler);
std::signal(SIGTERM, sigTermHandler); std::signal(SIGTERM, sigTermHandler);
if (disable_hidpi) { QString app_dir;
#ifdef __APPLE__ #ifdef __APPLE__
// Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering
QApplication tmp(argc, argv); QApplication tmp(argc, argv);
qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio() ).toLocal8Bit()); app_dir = QCoreApplication::applicationDirPath();
#endif if (disable_hidpi) {
qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit());
} }
#else
app_dir = QFileInfo(util::readlink("/proc/self/exe").c_str()).path();
#endif
qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150)); qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150));
// ensure the current dir matches the exectuable's directory // ensure the current dir matches the exectuable's directory
QApplication tmp(argc, argv); QDir::setCurrent(app_dir);
QString appDir = QCoreApplication::applicationDirPath();
QDir::setCurrent(appDir);
setQtSurfaceFormat(); setQtSurfaceFormat();
} }

@ -98,7 +98,7 @@ mat4 get_fit_view_transform(float widget_aspect_ratio, float frame_aspect_ratio)
} // namespace } // namespace
CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, bool zoom, QWidget* parent) : CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, bool zoom, QWidget* parent) :
stream_name(stream_name), requested_stream_type(type), zoomed_view(zoom), QOpenGLWidget(parent) { stream_name(stream_name), active_stream_type(type), requested_stream_type(type), zoomed_view(zoom), QOpenGLWidget(parent) {
setAttribute(Qt::WA_OpaquePaintEvent); setAttribute(Qt::WA_OpaquePaintEvent);
qRegisterMetaType<std::set<VisionStreamType>>("availableStreams"); qRegisterMetaType<std::set<VisionStreamType>>("availableStreams");
QObject::connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection); QObject::connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection);

@ -293,6 +293,18 @@
<source>Review</source> <source>Review</source>
<translation>مراجعة</translation> <translation>مراجعة</translation>
</message> </message>
<message>
<source>Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.</source>
<translation>اقرن جهازك بجهاز (connect.comma.ai) واحصل على عرضك من comma prime.</translation>
</message>
<message>
<source>Pair Device</source>
<translation>إقران الجهاز</translation>
</message>
<message>
<source>PAIR</source>
<translation>إقران</translation>
</message>
</context> </context>
<context> <context>
<name>DriverViewWindow</name> <name>DriverViewWindow</name>
@ -480,6 +492,29 @@
<translation> تنبيه</translation> <translation> تنبيه</translation>
</message> </message>
</context> </context>
<context>
<name>OnroadAlerts</name>
<message>
<source>openpilot Unavailable</source>
<translation>openpilot غير متوفر</translation>
</message>
<message>
<source>Waiting for controls to start</source>
<translation>في انتظار بدء عناصر التحكم</translation>
</message>
<message>
<source>TAKE CONTROL IMMEDIATELY</source>
<translation>تحكم على الفور</translation>
</message>
<message>
<source>Controls Unresponsive</source>
<translation>الضوابط غير مستجيبة</translation>
</message>
<message>
<source>Reboot Device</source>
<translation>إعادة التشغيل</translation>
</message>
</context>
<context> <context>
<name>PairingPopup</name> <name>PairingPopup</name>
<message> <message>
@ -615,6 +650,10 @@
<source>ft</source> <source>ft</source>
<translation>قدم</translation> <translation>قدم</translation>
</message> </message>
<message>
<source>now</source>
<translation>الآن</translation>
</message>
</context> </context>
<context> <context>
<name>Reset</name> <name>Reset</name>
@ -654,7 +693,7 @@ This may take up to a minute.</source>
</message> </message>
<message> <message>
<source>System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot.</source> <source>System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot.</source>
<translation type="unfinished"></translation> <translation>تم تفعيل إعادة ضبط النظام. اضغط على تأكيد لمسح جميع المحتويات والإعدادات. اضغط على إلغاء لاستئناف التمهيد.</translation>
</message> </message>
</context> </context>
<context> <context>
@ -764,15 +803,15 @@ This may take up to a minute.</source>
</message> </message>
<message> <message>
<source>Choose Software to Install</source> <source>Choose Software to Install</source>
<translation type="unfinished"></translation> <translation>اختر البرنامج للتثبيت</translation>
</message> </message>
<message> <message>
<source>openpilot</source> <source>openpilot</source>
<translation type="unfinished">openpilot</translation> <translation>openpilot</translation>
</message> </message>
<message> <message>
<source>Custom Software</source> <source>Custom Software</source>
<translation type="unfinished"></translation> <translation>البرمجيات المخصصة</translation>
</message> </message>
</context> </context>
<context> <context>
@ -1017,7 +1056,7 @@ This may take up to a minute.</source>
<name>TogglesPanel</name> <name>TogglesPanel</name>
<message> <message>
<source>Enable openpilot</source> <source>Enable openpilot</source>
<translation>تمكين</translation> <translation>تمكين openpilot</translation>
</message> </message>
<message> <message>
<source>Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off.</source> <source>Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off.</source>
@ -1149,7 +1188,7 @@ This may take up to a minute.</source>
</message> </message>
<message> <message>
<source>Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button.</source> <source>Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button.</source>
<translation type="unfinished"></translation> <translation>يوصى بالمعيار. في الوضع العدواني، سيتبع الطيار المفتوح السيارات الرائدة بشكل أقرب ويكون أكثر عدوانية مع البنزين والفرامل. في الوضع المريح، سيبقى openpilot بعيدًا عن السيارات الرائدة. في السيارات المدعومة، يمكنك التنقل بين هذه الشخصيات باستخدام زر مسافة عجلة القيادة.</translation>
</message> </message>
</context> </context>
<context> <context>

@ -293,6 +293,18 @@
<source>Review</source> <source>Review</source>
<translation>Überprüfen</translation> <translation>Überprüfen</translation>
</message> </message>
<message>
<source>Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.</source>
<translation>Koppele dein Gerät mit Comma Connect (connect.comma.ai) und sichere dir dein Comma Prime Angebot.</translation>
</message>
<message>
<source>Pair Device</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>DriverViewWindow</name> <name>DriverViewWindow</name>
@ -475,6 +487,29 @@
<translation> HINWEIS</translation> <translation> HINWEIS</translation>
</message> </message>
</context> </context>
<context>
<name>OnroadAlerts</name>
<message>
<source>openpilot Unavailable</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Waiting for controls to start</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>TAKE CONTROL IMMEDIATELY</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Controls Unresponsive</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reboot Device</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>PairingPopup</name> <name>PairingPopup</name>
<message> <message>
@ -598,6 +633,10 @@
<source>ft</source> <source>ft</source>
<translation>fuß</translation> <translation>fuß</translation>
</message> </message>
<message>
<source>now</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>Reset</name> <name>Reset</name>

@ -293,6 +293,18 @@
<source>Disengage to Power Off</source> <source>Disengage to Power Off</source>
<translation>Désengager pour éteindre</translation> <translation>Désengager pour éteindre</translation>
</message> </message>
<message>
<source>Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.</source>
<translation>Associez votre appareil avec comma connect (connect.comma.ai) et profitez de l&apos;offre comma prime.</translation>
</message>
<message>
<source>Pair Device</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>DriverViewWindow</name> <name>DriverViewWindow</name>
@ -476,6 +488,29 @@
<translation> ALERTE</translation> <translation> ALERTE</translation>
</message> </message>
</context> </context>
<context>
<name>OnroadAlerts</name>
<message>
<source>openpilot Unavailable</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Waiting for controls to start</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>TAKE CONTROL IMMEDIATELY</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Controls Unresponsive</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reboot Device</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>PairingPopup</name> <name>PairingPopup</name>
<message> <message>
@ -599,6 +634,10 @@
<source>ft</source> <source>ft</source>
<translation>ft</translation> <translation>ft</translation>
</message> </message>
<message>
<source>now</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>Reset</name> <name>Reset</name>

@ -293,6 +293,18 @@
<source>Review</source> <source>Review</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.</source>
<translation> comma connect (connect.comma.ai)comma primeの特典を申請してください</translation>
</message>
<message>
<source>Pair Device</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>DriverViewWindow</name> <name>DriverViewWindow</name>
@ -474,6 +486,29 @@
<translation> </translation> <translation> </translation>
</message> </message>
</context> </context>
<context>
<name>OnroadAlerts</name>
<message>
<source>openpilot Unavailable</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Waiting for controls to start</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>TAKE CONTROL IMMEDIATELY</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Controls Unresponsive</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reboot Device</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>PairingPopup</name> <name>PairingPopup</name>
<message> <message>
@ -594,6 +629,10 @@
<source>ft</source> <source>ft</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>now</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>Reset</name> <name>Reset</name>

@ -293,6 +293,18 @@
<source>Review</source> <source>Review</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.</source>
<translation> comma connect (connect.comma.ai) comma prime .</translation>
</message>
<message>
<source>Pair Device</source>
<translation> </translation>
</message>
<message>
<source>PAIR</source>
<translation></translation>
</message>
</context> </context>
<context> <context>
<name>DriverViewWindow</name> <name>DriverViewWindow</name>
@ -475,11 +487,34 @@
<translation> </translation> <translation> </translation>
</message> </message>
</context> </context>
<context>
<name>OnroadAlerts</name>
<message>
<source>openpilot Unavailable</source>
<translation> </translation>
</message>
<message>
<source>Waiting for controls to start</source>
<translation> </translation>
</message>
<message>
<source>TAKE CONTROL IMMEDIATELY</source>
<translation> </translation>
</message>
<message>
<source>Controls Unresponsive</source>
<translation> </translation>
</message>
<message>
<source>Reboot Device</source>
<translation> </translation>
</message>
</context>
<context> <context>
<name>PairingPopup</name> <name>PairingPopup</name>
<message> <message>
<source>Pair your device to your comma account</source> <source>Pair your device to your comma account</source>
<translation> comma </translation> <translation> comma </translation>
</message> </message>
<message> <message>
<source>Go to https://connect.comma.ai on your phone</source> <source>Go to https://connect.comma.ai on your phone</source>
@ -595,6 +630,10 @@
<source>ft</source> <source>ft</source>
<translation>ft</translation> <translation>ft</translation>
</message> </message>
<message>
<source>now</source>
<translation>now</translation>
</message>
</context> </context>
<context> <context>
<name>Reset</name> <name>Reset</name>

@ -293,6 +293,18 @@
<source>Review</source> <source>Review</source>
<translation>Revisar</translation> <translation>Revisar</translation>
</message> </message>
<message>
<source>Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.</source>
<translation>Pareie seu dispositivo com comma connect (connect.comma.ai) e reivindique sua oferta de comma prime.</translation>
</message>
<message>
<source>Pair Device</source>
<translation>Parear Dispositivo</translation>
</message>
<message>
<source>PAIR</source>
<translation>PAREAR</translation>
</message>
</context> </context>
<context> <context>
<name>DriverViewWindow</name> <name>DriverViewWindow</name>
@ -476,6 +488,29 @@
<translation> ALERTA</translation> <translation> ALERTA</translation>
</message> </message>
</context> </context>
<context>
<name>OnroadAlerts</name>
<message>
<source>openpilot Unavailable</source>
<translation>openpilot Indisponível</translation>
</message>
<message>
<source>Waiting for controls to start</source>
<translation>Aguardando controles para iniciar</translation>
</message>
<message>
<source>TAKE CONTROL IMMEDIATELY</source>
<translation>ASSUMA IMEDIATAMENTE</translation>
</message>
<message>
<source>Controls Unresponsive</source>
<translation>Controles Não Respondem</translation>
</message>
<message>
<source>Reboot Device</source>
<translation>Reinicie o Dispositivo</translation>
</message>
</context>
<context> <context>
<name>PairingPopup</name> <name>PairingPopup</name>
<message> <message>
@ -599,6 +634,10 @@
<source>ft</source> <source>ft</source>
<translation>pés</translation> <translation>pés</translation>
</message> </message>
<message>
<source>now</source>
<translation>agora</translation>
</message>
</context> </context>
<context> <context>
<name>Reset</name> <name>Reset</name>

@ -293,6 +293,18 @@
<source>Review</source> <source>Review</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.</source>
<translation> comma connect (connect.comma.ai) comma prime </translation>
</message>
<message>
<source>Pair Device</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>DriverViewWindow</name> <name>DriverViewWindow</name>
@ -475,6 +487,29 @@
<translation> </translation> <translation> </translation>
</message> </message>
</context> </context>
<context>
<name>OnroadAlerts</name>
<message>
<source>openpilot Unavailable</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Waiting for controls to start</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>TAKE CONTROL IMMEDIATELY</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Controls Unresponsive</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reboot Device</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>PairingPopup</name> <name>PairingPopup</name>
<message> <message>
@ -595,6 +630,10 @@
<source>ft</source> <source>ft</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>now</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>Reset</name> <name>Reset</name>

@ -293,6 +293,18 @@
<source>Review</source> <source>Review</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.</source>
<translation>Cihazınızı comma connect (connect.comma.ai) ile eşleştirin ve comma prime aboneliğine göz atın.</translation>
</message>
<message>
<source>Pair Device</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>DriverViewWindow</name> <name>DriverViewWindow</name>
@ -474,6 +486,29 @@
<translation> UYARI</translation> <translation> UYARI</translation>
</message> </message>
</context> </context>
<context>
<name>OnroadAlerts</name>
<message>
<source>openpilot Unavailable</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Waiting for controls to start</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>TAKE CONTROL IMMEDIATELY</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Controls Unresponsive</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reboot Device</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>PairingPopup</name> <name>PairingPopup</name>
<message> <message>
@ -594,6 +629,10 @@
<source>ft</source> <source>ft</source>
<translation>ft</translation> <translation>ft</translation>
</message> </message>
<message>
<source>now</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>Reset</name> <name>Reset</name>

@ -293,6 +293,18 @@
<source>Review</source> <source>Review</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.</source>
<translation>comma connect connect.comma.aicomma prime优惠</translation>
</message>
<message>
<source>Pair Device</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>DriverViewWindow</name> <name>DriverViewWindow</name>
@ -475,6 +487,29 @@
<translation> </translation> <translation> </translation>
</message> </message>
</context> </context>
<context>
<name>OnroadAlerts</name>
<message>
<source>openpilot Unavailable</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Waiting for controls to start</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>TAKE CONTROL IMMEDIATELY</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Controls Unresponsive</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reboot Device</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>PairingPopup</name> <name>PairingPopup</name>
<message> <message>
@ -595,6 +630,10 @@
<source>ft</source> <source>ft</source>
<translation>ft</translation> <translation>ft</translation>
</message> </message>
<message>
<source>now</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>Reset</name> <name>Reset</name>

@ -293,6 +293,18 @@
<source>Review</source> <source>Review</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.</source>
<translation> comma connect (connect.comma.ai) comma </translation>
</message>
<message>
<source>Pair Device</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>PAIR</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>DriverViewWindow</name> <name>DriverViewWindow</name>
@ -475,6 +487,29 @@
<translation> </translation> <translation> </translation>
</message> </message>
</context> </context>
<context>
<name>OnroadAlerts</name>
<message>
<source>openpilot Unavailable</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Waiting for controls to start</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>TAKE CONTROL IMMEDIATELY</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Controls Unresponsive</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reboot Device</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>PairingPopup</name> <name>PairingPopup</name>
<message> <message>
@ -595,6 +630,10 @@
<source>ft</source> <source>ft</source>
<translation>ft</translation> <translation>ft</translation>
</message> </message>
<message>
<source>now</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>Reset</name> <name>Reset</name>

@ -1,6 +1,5 @@
#pragma once #pragma once
#include <map>
#include <memory> #include <memory>
#include <string> #include <string>
@ -22,7 +21,6 @@ const int UI_HEADER_HEIGHT = 420;
const int UI_FREQ = 20; // Hz const int UI_FREQ = 20; // Hz
const int BACKLIGHT_OFFROAD = 50; const int BACKLIGHT_OFFROAD = 50;
typedef cereal::CarControl::HUDControl::AudibleAlert AudibleAlert;
const float MIN_DRAW_DISTANCE = 10.0; const float MIN_DRAW_DISTANCE = 10.0;
const float MAX_DRAW_DISTANCE = 100.0; const float MAX_DRAW_DISTANCE = 100.0;
@ -47,59 +45,6 @@ constexpr vec3 default_face_kpts_3d[] = {
{18.02, -49.14, 8.00}, {6.36, -51.20, 8.00}, {-5.98, -51.20, 8.00}, {18.02, -49.14, 8.00}, {6.36, -51.20, 8.00}, {-5.98, -51.20, 8.00},
}; };
struct Alert {
QString text1;
QString text2;
QString type;
cereal::ControlsState::AlertSize size;
cereal::ControlsState::AlertStatus status;
AudibleAlert sound;
bool equal(const Alert &a2) {
return text1 == a2.text1 && text2 == a2.text2 && type == a2.type && sound == a2.sound;
}
static Alert get(const SubMaster &sm, uint64_t started_frame) {
const cereal::ControlsState::Reader &cs = sm["controlsState"].getControlsState();
const uint64_t controls_frame = sm.rcv_frame("controlsState");
Alert alert = {};
if (controls_frame >= started_frame) { // Don't get old alert.
alert = {cs.getAlertText1().cStr(), cs.getAlertText2().cStr(),
cs.getAlertType().cStr(), cs.getAlertSize(),
cs.getAlertStatus(),
cs.getAlertSound()};
}
if (!sm.updated("controlsState") && (sm.frame - started_frame) > 5 * UI_FREQ) {
const int CONTROLS_TIMEOUT = 5;
const int controls_missing = (nanos_since_boot() - sm.rcv_time("controlsState")) / 1e9;
// Handle controls timeout
if (controls_frame < started_frame) {
// car is started, but controlsState hasn't been seen at all
alert = {"openpilot Unavailable", "Waiting for controls to start",
"controlsWaiting", cereal::ControlsState::AlertSize::MID,
cereal::ControlsState::AlertStatus::NORMAL,
AudibleAlert::NONE};
} else if (controls_missing > CONTROLS_TIMEOUT && !Hardware::PC()) {
// car is started, but controls is lagging or died
if (cs.getEnabled() && (controls_missing - CONTROLS_TIMEOUT) < 10) {
alert = {"TAKE CONTROL IMMEDIATELY", "Controls Unresponsive",
"controlsUnresponsive", cereal::ControlsState::AlertSize::FULL,
cereal::ControlsState::AlertStatus::CRITICAL,
AudibleAlert::WARNING_IMMEDIATE};
} else {
alert = {"Controls Unresponsive", "Reboot Device",
"controlsUnresponsivePermanent", cereal::ControlsState::AlertSize::MID,
cereal::ControlsState::AlertStatus::NORMAL,
AudibleAlert::NONE};
}
}
}
return alert;
}
};
typedef enum UIStatus { typedef enum UIStatus {
STATUS_DISENGAGED, STATUS_DISENGAGED,
@ -123,11 +68,6 @@ const QColor bg_colors [] = {
[STATUS_ENGAGED] = QColor(0x17, 0x86, 0x44, 0xf1), [STATUS_ENGAGED] = QColor(0x17, 0x86, 0x44, 0xf1),
}; };
static std::map<cereal::ControlsState::AlertStatus, QColor> alert_colors = {
{cereal::ControlsState::AlertStatus::NORMAL, QColor(0x15, 0x15, 0x15, 0xf1)},
{cereal::ControlsState::AlertStatus::USER_PROMPT, QColor(0xDA, 0x6F, 0x25, 0xf1)},
{cereal::ControlsState::AlertStatus::CRITICAL, QColor(0xC9, 0x22, 0x31, 0xf1)},
};
typedef struct UIScene { typedef struct UIScene {
bool calibration_valid = false; bool calibration_valid = false;

@ -19,7 +19,7 @@ from openpilot.common.time import system_time_valid
from openpilot.system.hardware import AGNOS, HARDWARE from openpilot.system.hardware import AGNOS, HARDWARE
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.controls.lib.alertmanager import set_offroad_alert from openpilot.selfdrive.controls.lib.alertmanager import set_offroad_alert
from openpilot.system.version import is_tested_branch from openpilot.system.version import get_build_metadata
LOCK_FILE = os.getenv("UPDATER_LOCK_FILE", "/tmp/safe_staging_overlay.lock") LOCK_FILE = os.getenv("UPDATER_LOCK_FILE", "/tmp/safe_staging_overlay.lock")
STAGING_ROOT = os.getenv("UPDATER_STAGING_ROOT", "/data/safe_staging") STAGING_ROOT = os.getenv("UPDATER_STAGING_ROOT", "/data/safe_staging")
@ -325,8 +325,9 @@ class Updater:
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
dt = now - last_update dt = now - last_update
build_metadata = get_build_metadata()
if failed_count > 15 and exception is not None and self.has_internet: if failed_count > 15 and exception is not None and self.has_internet:
if is_tested_branch(): if build_metadata.tested_channel:
extra_text = "Ensure the software is correctly installed. Uninstall and re-install if this error persists." extra_text = "Ensure the software is correctly installed. Uninstall and re-install if this error persists."
else: else:
extra_text = exception extra_text = exception

@ -10,7 +10,7 @@ from collections.abc import Generator
import requests import requests
import openpilot.system.hardware.tici.casync as casync import openpilot.system.updated.casync.casync as casync
SPARSE_CHUNK_FMT = struct.Struct('H2xI4x') SPARSE_CHUNK_FMT = struct.Struct('H2xI4x')
CAIBX_URL = "https://commadist.azureedge.net/agnosupdate/" CAIBX_URL = "https://commadist.azureedge.net/agnosupdate/"

@ -8,6 +8,7 @@ from typing import NoReturn
from timezonefinder import TimezoneFinder from timezonefinder import TimezoneFinder
import cereal.messaging as messaging import cereal.messaging as messaging
from openpilot.common.time import system_time_valid
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import AGNOS from openpilot.system.hardware import AGNOS
@ -69,7 +70,8 @@ def main() -> NoReturn:
while True: while True:
sm.update(1000) sm.update(1000)
msg = messaging.new_message('clocks', valid=True) msg = messaging.new_message('clocks')
msg.valid = system_time_valid()
msg.clocks.wallTimeNanos = time.time_ns() msg.clocks.wallTimeNanos = time.time_ns()
pm.send('clocks', msg) pm.send('clocks', msg)

@ -2,15 +2,19 @@
import io import io
import lzma import lzma
import os import os
import pathlib
import struct import struct
import sys import sys
import time import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from collections.abc import Callable from collections.abc import Callable
from typing import IO
import requests import requests
from Crypto.Hash import SHA512 from Crypto.Hash import SHA512
from openpilot.system.updated.casync import tar
from openpilot.system.updated.casync.common import create_casync_tar_package
CA_FORMAT_INDEX = 0x96824d9c7b129ff9 CA_FORMAT_INDEX = 0x96824d9c7b129ff9
CA_FORMAT_TABLE = 0xe75b9e112f17417d CA_FORMAT_TABLE = 0xe75b9e112f17417d
@ -37,20 +41,25 @@ class ChunkReader(ABC):
... ...
class FileChunkReader(ChunkReader): class BinaryChunkReader(ChunkReader):
"""Reads chunks from a local file""" """Reads chunks from a local file"""
def __init__(self, fn: str) -> None: def __init__(self, file_like: IO[bytes]) -> None:
super().__init__() super().__init__()
self.f = open(fn, 'rb') self.f = file_like
def __del__(self):
self.f.close()
def read(self, chunk: Chunk) -> bytes: def read(self, chunk: Chunk) -> bytes:
self.f.seek(chunk.offset) self.f.seek(chunk.offset)
return self.f.read(chunk.length) return self.f.read(chunk.length)
class FileChunkReader(BinaryChunkReader):
def __init__(self, path: str) -> None:
super().__init__(open(path, 'rb'))
def __del__(self):
self.f.close()
class RemoteChunkReader(ChunkReader): class RemoteChunkReader(ChunkReader):
"""Reads lzma compressed chunks from a remote store""" """Reads lzma compressed chunks from a remote store"""
@ -83,6 +92,20 @@ class RemoteChunkReader(ChunkReader):
return decompressor.decompress(contents) return decompressor.decompress(contents)
class DirectoryTarChunkReader(BinaryChunkReader):
"""creates a tar archive of a directory and reads chunks from it"""
def __init__(self, path: str, cache_file: str) -> None:
create_casync_tar_package(pathlib.Path(path), pathlib.Path(cache_file))
self.f = open(cache_file, "rb")
return super().__init__(self.f)
def __del__(self):
self.f.close()
os.unlink(self.f.name)
def parse_caibx(caibx_path: str) -> list[Chunk]: def parse_caibx(caibx_path: str) -> list[Chunk]:
"""Parses the chunks from a caibx file. Can handle both local and remote files. """Parses the chunks from a caibx file. Can handle both local and remote files.
Returns a list of chunks with hash, offset and length""" Returns a list of chunks with hash, offset and length"""
@ -181,6 +204,21 @@ def extract(target: list[Chunk],
return stats return stats
def extract_directory(target: list[Chunk],
sources: list[tuple[str, ChunkReader, ChunkDict]],
out_path: str,
tmp_file: str,
progress: Callable[[int], None] = None):
"""extract a directory stored as a casync tar archive"""
stats = extract(target, sources, tmp_file, progress)
with open(tmp_file, "rb") as f:
tar.extract_tar_archive(f, pathlib.Path(out_path))
return stats
def print_stats(stats: dict[str, int]): def print_stats(stats: dict[str, int]):
total_bytes = sum(stats.values()) total_bytes = sum(stats.values())
print(f"Total size: {total_bytes / 1024 / 1024:.2f} MB") print(f"Total size: {total_bytes / 1024 / 1024:.2f} MB")

@ -0,0 +1,55 @@
import dataclasses
import json
import pathlib
import subprocess
from openpilot.system.version import BUILD_METADATA_FILENAME, BuildMetadata
from openpilot.system.updated.casync import tar
CASYNC_ARGS = ["--with=symlinks", "--with=permissions", "--compression=xz", "--chunk-size=16M"]
CASYNC_FILES = [BUILD_METADATA_FILENAME]
def run(cmd):
return subprocess.check_output(cmd)
def get_exclude_set(path) -> set[str]:
exclude_set = set(CASYNC_FILES)
for file in path.rglob("*"):
if file.is_file() or file.is_symlink():
while file.resolve() != path.resolve():
exclude_set.add(str(file.relative_to(path)))
file = file.parent
return exclude_set
def create_build_metadata_file(path: pathlib.Path, build_metadata: BuildMetadata, channel: str):
with open(path / BUILD_METADATA_FILENAME, "w") as f:
build_metadata_dict = dataclasses.asdict(build_metadata)
build_metadata_dict["channel"] = channel
build_metadata_dict["openpilot"].pop("is_dirty") # this is determined at runtime
f.write(json.dumps(build_metadata_dict))
def is_not_git(path: pathlib.Path) -> bool:
return ".git" not in path.parts
def create_casync_tar_package(target_dir: pathlib.Path, output_path: pathlib.Path):
tar.create_tar_archive(output_path, target_dir, is_not_git)
def create_casync_release(target_dir: pathlib.Path, output_dir: pathlib.Path, caibx_name: str):
tar_file = output_dir / f"{caibx_name}.tar"
create_casync_tar_package(target_dir, tar_file)
caibx_file = output_dir / f"{caibx_name}.caibx"
run(["casync", "make", *CASYNC_ARGS, caibx_file, str(tar_file)])
tar_file.unlink()
digest = run(["casync", "digest", *CASYNC_ARGS, target_dir]).decode("utf-8").strip()
return digest, caibx_file

@ -0,0 +1,38 @@
import pathlib
import tarfile
from typing import IO, Callable
def include_default(_) -> bool:
return True
def create_tar_archive(filename: pathlib.Path, directory: pathlib.Path, include: Callable[[pathlib.Path], bool] = include_default):
"""Creates a tar archive of a directory"""
with tarfile.open(filename, 'w') as tar:
for file in sorted(directory.rglob("*"), key=lambda f: f.stat().st_size if f.is_file() else 0, reverse=True):
if not include(file):
continue
relative_path = str(file.relative_to(directory))
if file.is_symlink():
info = tarfile.TarInfo(relative_path)
info.type = tarfile.SYMTYPE
info.linkpath = str(file.readlink())
tar.addfile(info)
elif file.is_file():
info = tarfile.TarInfo(relative_path)
info.size = file.stat().st_size
info.type = tarfile.REGTYPE
info.mode = file.stat().st_mode
with file.open('rb') as f:
tar.addfile(info, f)
def extract_tar_archive(fh: IO[bytes], directory: pathlib.Path):
"""Extracts a tar archive to a directory"""
tar = tarfile.open(fileobj=fh, mode='r')
tar.extractall(str(directory), filter=lambda info, path: info)
tar.close()

@ -1,10 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import pathlib
import unittest import unittest
import tempfile import tempfile
import subprocess import subprocess
import openpilot.system.hardware.tici.casync as casync from openpilot.system.updated.casync import casync
from openpilot.system.updated.casync import tar
# dd if=/dev/zero of=/tmp/img.raw bs=1M count=2 # dd if=/dev/zero of=/tmp/img.raw bs=1M count=2
# sudo losetup -f /tmp/img.raw # sudo losetup -f /tmp/img.raw
@ -149,5 +151,117 @@ class TestCasync(unittest.TestCase):
self.assertLess(stats['remote'], len(self.contents)) self.assertLess(stats['remote'], len(self.contents))
class TestCasyncDirectory(unittest.TestCase):
"""Tests extracting a directory stored as a casync tar archive"""
NUM_FILES = 16
@classmethod
def setup_cache(cls, directory, files=None):
if files is None:
files = range(cls.NUM_FILES)
chunk_a = [i % 256 for i in range(1024)] * 512
chunk_b = [(256 - i) % 256 for i in range(1024)] * 512
zeroes = [0] * (1024 * 128)
cls.contents = chunk_a + chunk_b + zeroes + chunk_a
cls.contents = bytes(cls.contents)
for i in files:
with open(os.path.join(directory, f"file_{i}.txt"), "wb") as f:
f.write(cls.contents)
os.symlink(f"file_{i}.txt", os.path.join(directory, f"link_{i}.txt"))
@classmethod
def setUpClass(cls):
cls.tmpdir = tempfile.TemporaryDirectory()
# Create casync files
cls.manifest_fn = os.path.join(cls.tmpdir.name, 'orig.caibx')
cls.store_fn = os.path.join(cls.tmpdir.name, 'store')
cls.directory_to_extract = tempfile.TemporaryDirectory()
cls.setup_cache(cls.directory_to_extract.name)
cls.orig_fn = os.path.join(cls.tmpdir.name, 'orig.tar')
tar.create_tar_archive(cls.orig_fn, pathlib.Path(cls.directory_to_extract.name))
subprocess.check_output(["casync", "make", "--compression=xz", "--store", cls.store_fn, cls.manifest_fn, cls.orig_fn])
@classmethod
def tearDownClass(cls):
cls.tmpdir.cleanup()
cls.directory_to_extract.cleanup()
def setUp(self):
self.cache_dir = tempfile.TemporaryDirectory()
self.working_dir = tempfile.TemporaryDirectory()
self.out_dir = tempfile.TemporaryDirectory()
def tearDown(self):
self.cache_dir.cleanup()
self.working_dir.cleanup()
self.out_dir.cleanup()
def run_test(self):
target = casync.parse_caibx(self.manifest_fn)
cache_filename = os.path.join(self.working_dir.name, "cache.tar")
tmp_filename = os.path.join(self.working_dir.name, "tmp.tar")
sources = [('cache', casync.DirectoryTarChunkReader(self.cache_dir.name, cache_filename), casync.build_chunk_dict(target))]
sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))]
stats = casync.extract_directory(target, sources, pathlib.Path(self.out_dir.name), tmp_filename)
with open(os.path.join(self.out_dir.name, "file_0.txt"), "rb") as f:
self.assertEqual(f.read(), self.contents)
with open(os.path.join(self.out_dir.name, "link_0.txt"), "rb") as f:
self.assertEqual(f.read(), self.contents)
self.assertEqual(os.readlink(os.path.join(self.out_dir.name, "link_0.txt")), "file_0.txt")
return stats
def test_no_cache(self):
self.setup_cache(self.cache_dir.name, [])
stats = self.run_test()
self.assertGreater(stats['remote'], 0)
self.assertEqual(stats['cache'], 0)
def test_full_cache(self):
self.setup_cache(self.cache_dir.name, range(self.NUM_FILES))
stats = self.run_test()
self.assertEqual(stats['remote'], 0)
self.assertGreater(stats['cache'], 0)
def test_one_file_cache(self):
self.setup_cache(self.cache_dir.name, range(1))
stats = self.run_test()
self.assertGreater(stats['remote'], 0)
self.assertGreater(stats['cache'], 0)
self.assertLess(stats['cache'], stats['remote'])
def test_one_file_incorrect_cache(self):
self.setup_cache(self.cache_dir.name, range(self.NUM_FILES))
with open(os.path.join(self.cache_dir.name, "file_0.txt"), "wb") as f:
f.write(b"1234")
stats = self.run_test()
self.assertGreater(stats['remote'], 0)
self.assertGreater(stats['cache'], 0)
self.assertGreater(stats['cache'], stats['remote'])
def test_one_file_missing_cache(self):
self.setup_cache(self.cache_dir.name, range(self.NUM_FILES))
os.unlink(os.path.join(self.cache_dir.name, "file_12.txt"))
stats = self.run_test()
self.assertGreater(stats['remote'], 0)
self.assertGreater(stats['cache'], 0)
self.assertGreater(stats['cache'], stats['remote'])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

@ -1,17 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from dataclasses import dataclass
import json
import os import os
import pathlib
import subprocess import subprocess
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.utils import cache from openpilot.common.utils import cache
from openpilot.common.git import get_origin, get_branch, get_short_branch, get_normalized_origin, get_commit_date from openpilot.common.git import get_commit, get_origin, get_branch, get_short_branch, get_commit_date
RELEASE_BRANCHES = ['release3-staging', 'release3', 'nightly'] RELEASE_BRANCHES = ['release3-staging', 'release3', 'nightly']
TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging'] TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging']
BUILD_METADATA_FILENAME = "build.json"
training_version: bytes = b"0.2.0" training_version: bytes = b"0.2.0"
terms_version: bytes = b"2" terms_version: bytes = b"2"
@ -28,30 +33,12 @@ def get_release_notes(path: str = BASEDIR) -> str:
@cache @cache
def get_short_version() -> str: def is_prebuilt(path: str = BASEDIR) -> bool:
return get_version().split('-')[0] return os.path.exists(os.path.join(path, 'prebuilt'))
@cache
def is_prebuilt() -> bool:
return os.path.exists(os.path.join(BASEDIR, 'prebuilt'))
@cache @cache
def is_comma_remote() -> bool: def is_dirty(cwd: str = BASEDIR) -> bool:
# note to fork maintainers, this is used for release metrics. please do not
# touch this to get rid of the orange startup alert. there's better ways to do that
return get_normalized_origin() == "github.com/commaai/openpilot"
@cache
def is_tested_branch() -> bool:
return get_short_branch() in TESTED_BRANCHES
@cache
def is_release_branch() -> bool:
return get_short_branch() in RELEASE_BRANCHES
@cache
def is_dirty() -> bool:
origin = get_origin() origin = get_origin()
branch = get_branch() branch = get_branch()
if not origin or not branch: if not origin or not branch:
@ -60,14 +47,14 @@ def is_dirty() -> bool:
dirty = False dirty = False
try: try:
# Actually check dirty files # Actually check dirty files
if not is_prebuilt(): if not is_prebuilt(cwd):
# This is needed otherwise touched files might show up as modified # This is needed otherwise touched files might show up as modified
try: try:
subprocess.check_call(["git", "update-index", "--refresh"]) subprocess.check_call(["git", "update-index", "--refresh"], cwd=cwd)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
pass pass
dirty = (subprocess.call(["git", "diff-index", "--quiet", branch, "--"]) != 0) dirty = (subprocess.call(["git", "diff-index", "--quiet", branch, "--"], cwd=cwd)) != 0
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
cloudlog.exception("git subprocess failed while checking dirty") cloudlog.exception("git subprocess failed while checking dirty")
dirty = True dirty = True
@ -75,6 +62,97 @@ def is_dirty() -> bool:
return dirty return dirty
@dataclass
class OpenpilotMetadata:
version: str
release_notes: str
git_commit: str
git_origin: str
git_commit_date: str
build_style: str
is_dirty: bool # whether there are local changes
@property
def short_version(self) -> str:
return self.version.split('-')[0]
@property
def comma_remote(self) -> bool:
# note to fork maintainers, this is used for release metrics. please do not
# touch this to get rid of the orange startup alert. there's better ways to do that
return self.git_normalized_origin == "github.com/commaai/openpilot"
@property
def git_normalized_origin(self) -> str:
return self.git_origin \
.replace("git@", "", 1) \
.replace(".git", "", 1) \
.replace("https://", "", 1) \
.replace(":", "/", 1)
@dataclass
class BuildMetadata:
channel: str
openpilot: OpenpilotMetadata
@property
def tested_channel(self) -> bool:
return self.channel in TESTED_BRANCHES
@property
def release_channel(self) -> bool:
return self.channel in RELEASE_BRANCHES
@property
def canonical(self) -> str:
return f"{self.openpilot.version}-{self.openpilot.git_commit}-{self.openpilot.build_style}"
def build_metadata_from_dict(build_metadata: dict) -> BuildMetadata:
channel = build_metadata.get("channel", "unknown")
openpilot_metadata = build_metadata.get("openpilot", {})
version = openpilot_metadata.get("version", "unknown")
release_notes = openpilot_metadata.get("release_notes", "unknown")
git_commit = openpilot_metadata.get("git_commit", "unknown")
git_origin = openpilot_metadata.get("git_origin", "unknown")
git_commit_date = openpilot_metadata.get("git_commit_date", "unknown")
build_style = openpilot_metadata.get("build_style", "unknown")
return BuildMetadata(channel,
OpenpilotMetadata(
version=version,
release_notes=release_notes,
git_commit=git_commit,
git_origin=git_origin,
git_commit_date=git_commit_date,
build_style=build_style,
is_dirty=False))
def get_build_metadata(path: str = BASEDIR) -> BuildMetadata:
build_metadata_path = pathlib.Path(path) / BUILD_METADATA_FILENAME
if build_metadata_path.exists():
build_metadata = json.loads(build_metadata_path.read_text())
return build_metadata_from_dict(build_metadata)
git_folder = pathlib.Path(path) / ".git"
if git_folder.exists():
return BuildMetadata(get_short_branch(path),
OpenpilotMetadata(
version=get_version(path),
release_notes=get_release_notes(path),
git_commit=get_commit(path),
git_origin=get_origin(path),
git_commit_date=get_commit_date(path),
build_style="unknown",
is_dirty=is_dirty(path)))
cloudlog.exception("unable to get build metadata")
raise Exception("invalid build metadata")
if __name__ == "__main__": if __name__ == "__main__":
from openpilot.common.params import Params from openpilot.common.params import Params
@ -82,12 +160,4 @@ if __name__ == "__main__":
params.put("TermsVersion", terms_version) params.put("TermsVersion", terms_version)
params.put("TrainingVersion", training_version) params.put("TrainingVersion", training_version)
print(f"Dirty: {is_dirty()}") print(get_build_metadata())
print(f"Version: {get_version()}")
print(f"Short version: {get_short_version()}")
print(f"Origin: {get_origin()}")
print(f"Normalized origin: {get_normalized_origin()}")
print(f"Branch: {get_branch()}")
print(f"Short branch: {get_short_branch()}")
print(f"Prebuilt: {is_prebuilt()}")
print(f"Commit date: {get_commit_date()}")

@ -1 +1 @@
Subproject commit ab2f09706e8f64390e196f079ac69e67131b07f5 Subproject commit 3116a5053bc22b912254f1f7000ab9e267916cd5

@ -0,0 +1 @@
libyuv/

@ -1,10 +1,37 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
git clone https://chromium.googlesource.com/libyuv/libyuv DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
ARCHNAME=$(uname -m)
if [ -f /TICI ]; then
ARCHNAME="larch64"
fi
if [[ "$OSTYPE" == "darwin"* ]]; then
ARCHNAME="Darwin"
fi
cd $DIR
if [ ! -d libyuv ]; then
git clone --single-branch https://chromium.googlesource.com/libyuv/libyuv
fi
cd libyuv cd libyuv
git reset --hard 4a14cb2e81235ecd656e799aecaaf139db8ce4a2 git checkout 4a14cb2e81235ecd656e799aecaaf139db8ce4a2
# build
cmake . cmake .
make -j$(nproc)
INSTALL_DIR="$DIR/$ARCHNAME"
rm -rf $INSTALL_DIR
mkdir -p $INSTALL_DIR
rm -rf $DIR/include
mkdir -p $INSTALL_DIR/lib
cp $DIR/libyuv/libyuv.a $INSTALL_DIR/lib
cp -r $DIR/libyuv/include $DIR
## To create universal binary on Darwin: ## To create universal binary on Darwin:
## ``` ## ```

@ -3,35 +3,33 @@ set -e
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)"
ARCHNAME="x86_64" ARCHNAME=$(uname -m)
MAPLIBRE_FLAGS="-DMLN_QT_WITH_LOCATION=OFF" MAPLIBRE_FLAGS="-DMLN_QT_WITH_LOCATION=OFF"
if [ -f /AGNOS ]; then if [ -f /TICI ]; then
ARCHNAME="larch64" ARCHNAME="larch64"
#MAPLIBRE_FLAGS="$MAPLIBRE_FLAGS -DCMAKE_SYSTEM_NAME=Android -DANDROID_ABI=arm64-v8a" #MAPLIBRE_FLAGS="$MAPLIBRE_FLAGS -DCMAKE_SYSTEM_NAME=Android -DANDROID_ABI=arm64-v8a"
fi fi
cd $DIR cd $DIR
if [ ! -d maplibre ]; then if [ ! -d maplibre ]; then
git clone git@github.com:maplibre/maplibre-native-qt.git $DIR/maplibre git clone --single-branch https://github.com/maplibre/maplibre-native-qt.git $DIR/maplibre
fi fi
cd maplibre cd maplibre
git fetch --all
git checkout 3726266e127c1f94ad64837c9dbe03d238255816 git checkout 3726266e127c1f94ad64837c9dbe03d238255816
git submodule update --depth=1 --recursive --init git submodule update --depth=1 --recursive --init
# build # build
mkdir -p build mkdir -p build
cd build cd build
set -x
cmake $MAPLIBRE_FLAGS $DIR/maplibre cmake $MAPLIBRE_FLAGS $DIR/maplibre
make -j$(nproc) || make -j2 || make -j1 make -j$(nproc)
INSTALL_DIR="$DIR/$ARCHNAME" INSTALL_DIR="$DIR/$ARCHNAME"
rm -rf $INSTALL_DIR rm -rf $INSTALL_DIR
mkdir -p $INSTALL_DIR mkdir -p $INSTALL_DIR
rm -rf $INSTALL_DIR/lib $DIR/include rm -rf $DIR/include
mkdir -p $INSTALL_DIR/lib $INSTALL_DIR/include $DIR/include mkdir -p $INSTALL_DIR/lib $INSTALL_DIR/include $DIR/include
cp -r $DIR/maplibre/build/src/core/*.so* $INSTALL_DIR/lib cp -r $DIR/maplibre/build/src/core/*.so* $INSTALL_DIR/lib
cp -r $DIR/maplibre/build/src/core/include/* $INSTALL_DIR/include cp -r $DIR/maplibre/build/src/core/include/* $INSTALL_DIR/include

@ -80,8 +80,8 @@ void DBCFile::parse(const QString &content) {
static QRegularExpression bo_regexp(R"(^BO_ (\w+) (\w+) *: (\w+) (\w+))"); static QRegularExpression bo_regexp(R"(^BO_ (\w+) (\w+) *: (\w+) (\w+))");
static QRegularExpression sg_regexp(R"(^SG_ (\w+) : (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))"); static QRegularExpression sg_regexp(R"(^SG_ (\w+) : (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))");
static QRegularExpression sgm_regexp(R"(^SG_ (\w+) (\w+) *: (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))"); static QRegularExpression sgm_regexp(R"(^SG_ (\w+) (\w+) *: (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))");
static QRegularExpression msg_comment_regexp(R"(^CM_ BO_ *(\w+) *\"([^"]*)\"\s*;)"); static QRegularExpression msg_comment_regexp(R"(^CM_ BO_ *(\w+) *\"((?:[^"\\]|\\.)*)\"\s*;)");
static QRegularExpression sg_comment_regexp(R"(^CM_ SG_ *(\w+) *(\w+) *\"([^"]*)\"\s*;)"); static QRegularExpression sg_comment_regexp(R"(^CM_ SG_ *(\w+) *(\w+) *\"((?:[^"\\]|\\.)*)\"\s*;)");
static QRegularExpression val_regexp(R"(VAL_ (\w+) (\w+) (\s*[-+]?[0-9]+\s+\".+?\"[^;]*))"); static QRegularExpression val_regexp(R"(VAL_ (\w+) (\w+) (\s*[-+]?[0-9]+\s+\".+?\"[^;]*))");
int line_num = 0; int line_num = 0;
@ -173,7 +173,7 @@ void DBCFile::parse(const QString &content) {
auto match = msg_comment_regexp.match(line); auto match = msg_comment_regexp.match(line);
dbc_assert(match.hasMatch()); dbc_assert(match.hasMatch());
if (auto m = (cabana::Msg *)msg(match.captured(1).toUInt())) { if (auto m = (cabana::Msg *)msg(match.captured(1).toUInt())) {
m->comment = match.captured(2).trimmed(); m->comment = match.captured(2).trimmed().replace("\\\"", "\"");
} }
} else if (line.startsWith("CM_ SG_ ")) { } else if (line.startsWith("CM_ SG_ ")) {
if (!line.endsWith("\";")) { if (!line.endsWith("\";")) {
@ -183,7 +183,7 @@ void DBCFile::parse(const QString &content) {
auto match = sg_comment_regexp.match(line); auto match = sg_comment_regexp.match(line);
dbc_assert(match.hasMatch()); dbc_assert(match.hasMatch());
if (auto s = get_sig(match.captured(1).toUInt(), match.captured(2))) { if (auto s = get_sig(match.captured(1).toUInt(), match.captured(2))) {
s->comment = match.captured(3).trimmed(); s->comment = match.captured(3).trimmed().replace("\\\"", "\"");
} }
} else { } else {
seen = false; seen = false;
@ -207,7 +207,7 @@ QString DBCFile::generateDBC() {
const QString transmitter = m.transmitter.isEmpty() ? DEFAULT_NODE_NAME : m.transmitter; const QString transmitter = m.transmitter.isEmpty() ? DEFAULT_NODE_NAME : m.transmitter;
dbc_string += QString("BO_ %1 %2: %3 %4\n").arg(address).arg(m.name).arg(m.size).arg(transmitter); dbc_string += QString("BO_ %1 %2: %3 %4\n").arg(address).arg(m.name).arg(m.size).arg(transmitter);
if (!m.comment.isEmpty()) { if (!m.comment.isEmpty()) {
comment += QString("CM_ BO_ %1 \"%2\";\n").arg(address).arg(m.comment); comment += QString("CM_ BO_ %1 \"%2\";\n").arg(address).arg(QString(m.comment).replace("\"", "\\\""));
} }
for (auto sig : m.getSignals()) { for (auto sig : m.getSignals()) {
QString multiplexer_indicator; QString multiplexer_indicator;
@ -230,7 +230,7 @@ QString DBCFile::generateDBC() {
.arg(sig->unit) .arg(sig->unit)
.arg(sig->receiver_name.isEmpty() ? DEFAULT_NODE_NAME : sig->receiver_name); .arg(sig->receiver_name.isEmpty() ? DEFAULT_NODE_NAME : sig->receiver_name);
if (!sig->comment.isEmpty()) { if (!sig->comment.isEmpty()) {
comment += QString("CM_ SG_ %1 %2 \"%3\";\n").arg(address).arg(sig->name).arg(sig->comment); comment += QString("CM_ SG_ %1 %2 \"%3\";\n").arg(address).arg(sig->name).arg(QString(sig->comment).replace("\"", "\\\""));
} }
if (!sig->val_desc.empty()) { if (!sig->val_desc.empty()) {
QStringList text; QStringList text;

@ -2,11 +2,17 @@
import argparse import argparse
import json import json
from openpilot.selfdrive.car.values import create_platform_map from openpilot.selfdrive.car.fingerprints import MIGRATION
from openpilot.selfdrive.car.values import PLATFORMS
def generate_dbc_json() -> str: def generate_dbc_json() -> str:
dbc_map = create_platform_map(lambda platform: platform.config.dbc_dict["pt"] if platform != "MOCK" else None) dbc_map = {platform.name: platform.config.dbc_dict['pt'] for platform in PLATFORMS.values() if platform != "MOCK"}
for m in MIGRATION:
if MIGRATION[m] in dbc_map:
dbc_map[m] = dbc_map[MIGRATION[m]]
return json.dumps(dict(sorted(dbc_map.items())), indent=2) return json.dumps(dict(sorted(dbc_map.items())), indent=2)

@ -65,6 +65,17 @@ CM_ SG_ 160 signal_1 "signal comment";
REQUIRE(dbc.generateDBC() == content); REQUIRE(dbc.generateDBC() == content);
} }
TEST_CASE("DBCFile::generateDBC - escaped quotes") {
QString content = R"(BO_ 160 message_1: 8 EON
SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX
CM_ BO_ 160 "message comment with \"escaped quotes\"";
CM_ SG_ 160 signal_1 "signal comment with \"escaped quotes\"";
)";
DBCFile dbc("", content);
REQUIRE(dbc.generateDBC() == content);
}
TEST_CASE("parse_dbc") { TEST_CASE("parse_dbc") {
QString content = R"( QString content = R"(
BO_ 160 message_1: 8 EON BO_ 160 message_1: 8 EON
@ -82,7 +93,11 @@ CM_ SG_ 160 signal_1 "signal comment";
CM_ SG_ 160 signal_2 "multiple line comment CM_ SG_ 160 signal_2 "multiple line comment
1 1
2 2
";)"; ";
CM_ BO_ 162 "message comment with \"escaped quotes\"";
CM_ SG_ 162 signal_1 "signal comment with \"escaped quotes\"";
)";
DBCFile file("", content); DBCFile file("", content);
auto msg = file.msg(160); auto msg = file.msg(160);
@ -121,6 +136,10 @@ CM_ SG_ 160 signal_2 "multiple line comment
REQUIRE(msg->sigs[1]->start_bit == 12); REQUIRE(msg->sigs[1]->start_bit == 12);
REQUIRE(msg->sigs[1]->size == 1); REQUIRE(msg->sigs[1]->size == 1);
REQUIRE(msg->sigs[1]->receiver_name == "XXX"); REQUIRE(msg->sigs[1]->receiver_name == "XXX");
// escaped quotes
REQUIRE(msg->comment == "message comment with \"escaped quotes\"");
REQUIRE(msg->sigs[0]->comment == "signal comment with \"escaped quotes\"");
} }
TEST_CASE("parse_opendbc") { TEST_CASE("parse_opendbc") {

@ -0,0 +1,304 @@
#!/usr/bin/env python3
import sys
import json
import base64
import os
import subprocess
from multiprocessing import Pool
from openpilot.tools.lib.route import Route
from openpilot.tools.lib.logreader import LogReader
try:
from mcap.writer import Writer, CompressionType
except ImportError:
print("mcap module not found. Attempting to install...")
subprocess.run([sys.executable, "-m", "pip", "install", "mcap"])
# Attempt to import again after installation
try:
from mcap.writer import Writer, CompressionType
except ImportError:
print("Failed to install mcap module. Exiting.")
sys.exit(1)
FOXGLOVE_IMAGE_SCHEME_TITLE = "foxglove.CompressedImage"
FOXGLOVE_GEOJSON_TITLE = "foxglove.GeoJSON"
FOXGLOVE_IMAGE_ENCODING = "base64"
OUT_MCAP_FILE_NAME = "json_log.mcap"
RLOG_FOLDER = "rlogs"
SCHEMAS_FOLDER = "schemas"
SCHEMA_EXTENSION = ".json"
schemas: dict[str, int] = {}
channels: dict[str, int] = {}
writer: Writer
def convertBytesToString(data):
if isinstance(data, bytes):
return data.decode('latin-1') # Assuming UTF-8 encoding, adjust if needed
elif isinstance(data, list):
return [convertBytesToString(item) for item in data]
elif isinstance(data, dict):
return {key: convertBytesToString(value) for key, value in data.items()}
else:
return data
# Load jsonscheme for every Event
def loadSchema(schemaName):
with open(os.path.join(SCHEMAS_FOLDER, schemaName + SCHEMA_EXTENSION), "r") as file:
return json.loads(file.read())
# Foxglove creates one graph of an array, and not one for each item of an array
# This can be avoided by transforming array to separate objects
def transformListsToJsonDict(json_data):
def convert_array_to_dict(array):
new_dict = {}
for index, item in enumerate(array):
if isinstance(item, dict):
new_dict[index] = transformListsToJsonDict(item)
else:
new_dict[index] = item
return new_dict
new_data = {}
for key, value in json_data.items():
if isinstance(value, list):
new_data[key] = convert_array_to_dict(value)
elif isinstance(value, dict):
new_data[key] = transformListsToJsonDict(value)
else:
new_data[key] = value
return new_data
# Transform openpilot thumbnail to foxglove compressedImage
def transformToFoxgloveSchema(jsonMsg):
bytesImgData = jsonMsg.get("thumbnail").get("thumbnail").encode('latin1')
base64ImgData = base64.b64encode(bytesImgData)
base64_string = base64ImgData.decode('utf-8')
foxMsg = {
"timestamp": {"sec": "0", "nsec": jsonMsg.get("logMonoTime")},
"frame_id": str(jsonMsg.get("thumbnail").get("frameId")),
"data": base64_string,
"format": "jpeg",
}
return foxMsg
# TODO: Check if there is a tool to build GEOJson
def transformMapCoordinates(jsonMsg):
coordinates = []
for jsonCoords in jsonMsg.get("navRoute").get("coordinates"):
coordinates.append([jsonCoords.get("longitude"), jsonCoords.get("latitude")])
# Define the GeoJSON
geojson_data = {
"type": "FeatureCollection",
"features": [{"type": "Feature", "geometry": {"type": "LineString", "coordinates": coordinates}, "logMonoTime": jsonMsg.get("logMonoTime")}],
}
# Create the final JSON with the GeoJSON data encoded as a string
geoJson = {"geojson": json.dumps(geojson_data)}
return geoJson
def jsonToScheme(jsonData):
zeroArray = False
schema = {"type": "object", "properties": {}, "required": []}
for key, value in jsonData.items():
if isinstance(value, dict):
tempScheme, zeroArray = jsonToScheme(value)
if tempScheme == 0:
return 0
schema["properties"][key] = tempScheme
schema["required"].append(key)
elif isinstance(value, list):
if all(isinstance(item, dict) for item in value) and len(value) > 0: # Handle zero value arrays
# Handle array of objects
tempScheme, zeroArray = jsonToScheme(value[0])
schema["properties"][key] = {"type": "array", "items": tempScheme if value else {}}
schema["required"].append(key)
else:
if len(value) == 0:
zeroArray = True
# Handle array of primitive types
schema["properties"][key] = {"type": "array", "items": {"type": "string"}}
schema["required"].append(key)
else:
typeName = type(value).__name__
if typeName == "str":
typeName = "string"
elif typeName == "bool":
typeName = "boolean"
elif typeName == "float":
typeName = "number"
elif typeName == "int":
typeName = "integer"
schema["properties"][key] = {"type": typeName}
schema["required"].append(key)
return schema, zeroArray
def saveScheme(scheme, schemaFileName):
schemaFileName = schemaFileName + SCHEMA_EXTENSION
# Create the new schemas folder
os.makedirs(SCHEMAS_FOLDER, exist_ok=True)
with open(os.path.join(SCHEMAS_FOLDER, schemaFileName), 'w') as json_file:
json.dump(convertBytesToString(scheme), json_file)
def convertToFoxGloveFormat(jsonData, rlogTopic):
jsonData["title"] = rlogTopic
if rlogTopic == "thumbnail":
jsonData = transformToFoxgloveSchema(jsonData)
jsonData["title"] = FOXGLOVE_IMAGE_SCHEME_TITLE
elif rlogTopic == "navRoute":
jsonData = transformMapCoordinates(jsonData)
jsonData["title"] = FOXGLOVE_GEOJSON_TITLE
else:
jsonData = transformListsToJsonDict(jsonData)
return jsonData
def generateSchemas():
listOfDirs = os.listdir(RLOG_FOLDER)
# Open every dir in rlogs
for directory in listOfDirs:
# List every file in every rlog dir
dirPath = os.path.join(RLOG_FOLDER, directory)
listOfFiles = os.listdir(dirPath)
lastIteration = len(listOfFiles)
for iteration, file in enumerate(listOfFiles):
# Load json data from every file until found one without empty arrays
filePath = os.path.join(dirPath, file)
with open(filePath, 'r') as jsonFile:
jsonData = json.load(jsonFile)
scheme, zerroArray = jsonToScheme(jsonData)
# If array of len 0 has been found, type of its data can not be parsed, skip to the next log
# in search for a non empty array. If there is not an non empty array in logs, put a dummy string type
if zerroArray and not iteration == lastIteration - 1:
continue
title = jsonData.get("title")
scheme["title"] = title
# Add contentEncoding type, hardcoded in foxglove format
if title == FOXGLOVE_IMAGE_SCHEME_TITLE:
scheme["properties"]["data"]["contentEncoding"] = FOXGLOVE_IMAGE_ENCODING
saveScheme(scheme, directory)
break
def downloadLogs(logPaths):
segment_counter = 0
for logPath in logPaths:
segment_counter += 1
msg_counter = 1
print(segment_counter)
rlog = LogReader(logPath)
for msg in rlog:
jsonMsg = json.loads(json.dumps(convertBytesToString(msg.to_dict())))
jsonMsg = convertToFoxGloveFormat(jsonMsg, msg.which())
rlog_dir_path = os.path.join(RLOG_FOLDER, msg.which())
if not os.path.exists(rlog_dir_path):
os.makedirs(rlog_dir_path)
file_path = os.path.join(rlog_dir_path, str(segment_counter) + "," + str(msg_counter))
with open(file_path, 'w') as json_file:
json.dump(jsonMsg, json_file)
msg_counter += 1
def getLogMonoTime(jsonMsg):
if jsonMsg.get("title") == FOXGLOVE_IMAGE_SCHEME_TITLE:
logMonoTime = jsonMsg.get("timestamp").get("nsec")
elif jsonMsg.get("title") == FOXGLOVE_GEOJSON_TITLE:
logMonoTime = json.loads(jsonMsg.get("geojson")).get("features")[0].get("logMonoTime")
else:
logMonoTime = jsonMsg.get("logMonoTime")
return logMonoTime
def processMsgs(args):
msgFile, rlogTopicPath, rlogTopic = args
msgFilePath = os.path.join(rlogTopicPath, msgFile)
with open(msgFilePath, "r") as file:
jsonMsg = json.load(file)
logMonoTime = getLogMonoTime(jsonMsg)
return {'channel_id': channels[rlogTopic], 'log_time': logMonoTime, 'data': json.dumps(jsonMsg).encode("utf-8"), 'publish_time': logMonoTime}
# Get logs from a path, and convert them into mcap
def createMcap(logPaths):
print(f"Downloading logs [{len(logPaths)}]")
downloadLogs(logPaths)
print("Creating schemas")
generateSchemas()
print("Creating mcap file")
listOfRlogTopics = os.listdir(RLOG_FOLDER)
print(f"Registering schemas and channels [{len(listOfRlogTopics)}]")
for counter, rlogTopic in enumerate(listOfRlogTopics):
print(counter)
schema = loadSchema(rlogTopic)
schema_id = writer.register_schema(name=schema.get("title"), encoding="jsonschema", data=json.dumps(schema).encode())
schemas[rlogTopic] = schema_id
channel_id = writer.register_channel(schema_id=schemas[rlogTopic], topic=rlogTopic, message_encoding="json")
channels[rlogTopic] = channel_id
rlogTopicPath = os.path.join(RLOG_FOLDER, rlogTopic)
msgFiles = os.listdir(rlogTopicPath)
pool = Pool()
results = pool.map(processMsgs, [(msgFile, rlogTopicPath, rlogTopic) for msgFile in msgFiles])
pool.close()
pool.join()
for result in results:
writer.add_message(channel_id=result['channel_id'], log_time=result['log_time'], data=result['data'], publish_time=result['publish_time'])
def is_program_installed(program_name):
try:
# Check if the program is installed using dpkg (for traditional Debian packages)
subprocess.run(["dpkg", "-l", program_name], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return True
except subprocess.CalledProcessError:
# Check if the program is installed using snap
try:
subprocess.run(["snap", "list", program_name], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return True
except subprocess.CalledProcessError:
return False
if __name__ == '__main__':
# Example usage:
program_name = "foxglove-studio" # Change this to the program you want to check
if is_program_installed(program_name):
print(f"{program_name} detected.")
else:
print(f"{program_name} could not be detected.")
installFoxglove = input("Would you like to install it? YES/NO? - ")
if installFoxglove.lower() == "yes":
try:
subprocess.run(['./install_foxglove.sh'], check=True)
print("Installation completed successfully.")
except subprocess.CalledProcessError as e:
print(f"Installation failed with return code {e.returncode}.")
# Get a route
if len(sys.argv) == 1:
route_name = "a2a0ccea32023010|2023-07-27--13-01-19"
print("No route was provided, using demo route")
else:
route_name = sys.argv[1]
# Get logs for a route
print("Getting route log paths")
route = Route(route_name)
logPaths = route.log_paths()
# Start mcap writer
with open(OUT_MCAP_FILE_NAME, "wb") as stream:
writer = Writer(stream, compression=CompressionType.NONE)
writer.start()
createMcap(logPaths)
writer.finish()
print(f"File {OUT_MCAP_FILE_NAME} has been successfully created. Please import it into foxglove studio to continue.")

@ -0,0 +1,3 @@
#!/bin/bash
echo "Installing foxglvoe studio..."
sudo snap install foxglove-studio

@ -75,6 +75,7 @@ function install_ubuntu_common_requirements() {
libqt5charts5-dev \ libqt5charts5-dev \
libqt5serialbus5-dev \ libqt5serialbus5-dev \
libqt5x11extras5-dev \ libqt5x11extras5-dev \
libqt5opengl5-dev \
libreadline-dev \ libreadline-dev \
libdw1 \ libdw1 \
valgrind valgrind

@ -1,7 +1,4 @@
import bz2 import bz2
import datetime
TIME_FMT = "%Y-%m-%d--%H-%M-%S"
# regex patterns # regex patterns
@ -23,13 +20,6 @@ class RE:
OP_SEGMENT_DIR = fr'^(?P<segment_name>{SEGMENT_NAME})$' OP_SEGMENT_DIR = fr'^(?P<segment_name>{SEGMENT_NAME})$'
def timestamp_to_datetime(t: str) -> datetime.datetime:
"""
Convert an openpilot route timestamp to a python datetime
"""
return datetime.datetime.strptime(t, TIME_FMT)
def save_log(dest, log_msgs, compress=True): def save_log(dest, log_msgs, compress=True):
dat = b"".join(msg.as_builder().to_bytes() for msg in log_msgs) dat = b"".join(msg.as_builder().to_bytes() for msg in log_msgs)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save