diff --git a/.github/labeler.yaml b/.github/labeler.yaml index e91764bb3..278ac5803 100644 --- a/.github/labeler.yaml +++ b/.github/labeler.yaml @@ -1,6 +1,6 @@ ci: - changed-files: - - any-glob-to-all-files: "{.github/**,**/test_*,Jenkinsfile}" + - any-glob-to-all-files: "{.github/**,**/test_*,**/test/**,Jenkinsfile}" chore: - changed-files: diff --git a/.github/workflows/jenkins-pr-trigger.yaml b/.github/workflows/jenkins-pr-trigger.yaml index db1d52401..14e2fdf49 100644 --- a/.github/workflows/jenkins-pr-trigger.yaml +++ b/.github/workflows/jenkins-pr-trigger.yaml @@ -9,6 +9,9 @@ jobs: scan-comments: runs-on: ubuntu-latest if: ${{ github.event.issue.pull_request }} + permissions: + contents: write + issues: write steps: - name: Check for trigger phrase id: check_comment @@ -43,3 +46,14 @@ jobs: git config --global user.email "github-actions[bot]@users.noreply.github.com" git checkout -b tmp-jenkins-${{ github.event.issue.number }} GIT_LFS_SKIP_PUSH=1 git push -f origin tmp-jenkins-${{ github.event.issue.number }} + + - name: Delete trigger comment + if: steps.check_comment.outputs.result == 'true' && always() + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + }); diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 13d199096..912ac787c 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -189,7 +189,8 @@ jobs: - name: Run unit tests timeout-minutes: ${{ contains(runner.name, 'nsc') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 999 }} run: | - ${{ env.RUN }} "$PYTEST --collect-only -m 'not slow' &> /dev/null && \ + ${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \ + $PYTEST --collect-only -m 'not slow' &> /dev/null && \ MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \ ./selfdrive/ui/tests/create_test_translations.sh && \ QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \ diff --git a/.gitignore b/.gitignore index db569f06c..3c0dad521 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,11 @@ venv/ model2.png a.out .hypothesis +.cache/ /docs_site/ +*.mp4 *.dylib *.DSYM *.d diff --git a/RELEASES.md b/RELEASES.md index 58d0d07a7..545152e0f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,10 +1,14 @@ -Version 0.10.0 (2025-07-07) +Version 0.10.0 (2025-08-05) ======================== * New driving model - * Lead car ground-truth fixes - * Ported over VAE from the MLSIM stack - * New training architecture described in CVPR paper + * New training architecture + * Architecture outlined in CVPR paper: "Learning to Drive from a World Model" + * Longitudinal MPC replaced by E2E planning from worldmodel in experimental mode + * Action from lateral MPC as training objective replaced by E2E planning from worldmodel + * Low-speed lead car ground-truth fixes + * Enable live-learned steering actuation delay +* Record driving feedback using LKAS button * Opt-in audio recording for dashcam video Version 0.9.9 (2025-05-23) diff --git a/SConstruct b/SConstruct index 2817a46d3..192f83a0f 100644 --- a/SConstruct +++ b/SConstruct @@ -17,7 +17,7 @@ AGNOS = TICI Decider('MD5-timestamp') -SetOption('num_jobs', int(os.cpu_count()/2)) +SetOption('num_jobs', max(1, int(os.cpu_count()/2))) AddOption('--kaitai', action='store_true', diff --git a/cereal/log.capnp b/cereal/log.capnp index 1ecefa5ad..15795f8c3 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -127,8 +127,9 @@ struct OnroadEvent @0xc4fa6047f024e718 { espActive @90; personalityChanged @91; aeb @92; - userFlag @95; + userBookmark @95; excessiveActuation @96; + audioFeedback @97; soundsUnavailableDEPRECATED @47; } @@ -2468,7 +2469,7 @@ struct DebugAlert { alertText2 @1 :Text; } -struct UserFlag { +struct UserBookmark @0xfe346a9de48d9b50 { } struct SoundPressure @0xdc24138990726023 { @@ -2486,6 +2487,11 @@ struct AudioData { sampleRate @1 :UInt32; } +struct AudioFeedback { + audio @0 :AudioData; + blockNum @1 :UInt16; +} + struct Touch { sec @0 :Int64; usec @1 :Int64; @@ -2586,9 +2592,13 @@ struct Event { mapRenderState @105: MapRenderState; # UI services - userFlag @93 :UserFlag; uiDebug @102 :UIDebug; + # driving feedback + userBookmark @93 :UserBookmark; + bookmarkButton @148 :UserBookmark; + audioFeedback @149 :AudioFeedback; + # *********** debug *********** testJoystick @52 :Joystick; roadEncodeData @86 :EncodeData; diff --git a/cereal/messaging/tests/test_messaging.py b/cereal/messaging/tests/test_messaging.py index 236922998..583eb8b0d 100644 --- a/cereal/messaging/tests/test_messaging.py +++ b/cereal/messaging/tests/test_messaging.py @@ -177,8 +177,8 @@ class TestMessaging: # wait 5 socket timeouts before sending msg = random_carstate() - delayed_send(sock_timeout*5, pub_sock, msg.to_bytes()) start_time = time.monotonic() + delayed_send(sock_timeout*5, pub_sock, msg.to_bytes()) recvd = messaging.recv_one_retry(sub_sock) assert (time.monotonic() - start_time) >= sock_timeout*5 assert isinstance(recvd, capnp._DynamicStructReader) diff --git a/cereal/messaging/tests/test_pub_sub_master.py b/cereal/messaging/tests/test_pub_sub_master.py index e47e71339..5e26b4970 100644 --- a/cereal/messaging/tests/test_pub_sub_master.py +++ b/cereal/messaging/tests/test_pub_sub_master.py @@ -86,7 +86,7 @@ class TestSubMaster: "cameraOdometry": (20, 10), "liveCalibration": (4, 4), "carParams": (None, None), - "userFlag": (None, None), + "userBookmark": (None, None), } for service, (max_freq, min_freq) in checks.items(): diff --git a/cereal/services.py b/cereal/services.py index 4418d079a..373e865e3 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -72,9 +72,11 @@ _services: dict[str, tuple] = { "navRoute": (True, 0.), "navThumbnail": (True, 0.), "qRoadEncodeIdx": (False, 20.), - "userFlag": (True, 0., 1), + "userBookmark": (True, 0., 1), "soundPressure": (True, 10., 10), "rawAudioData": (False, 20.), + "bookmarkButton": (True, 0., 1), + "audioFeedback": (True, 0., 1), # sunnypilot "modelManagerSP": (False, 1., 1), diff --git a/common/conversions.py b/common/constants.py similarity index 83% rename from common/conversions.py rename to common/constants.py index b02b33c62..7ca425c4b 100644 --- a/common/conversions.py +++ b/common/constants.py @@ -1,6 +1,7 @@ import numpy as np -class Conversions: +# conversions +class CV: # Speed MPH_TO_KPH = 1.609344 KPH_TO_MPH = 1. / MPH_TO_KPH @@ -17,3 +18,6 @@ class Conversions: # Mass LB_TO_KG = 0.453592 + + +ACCELERATION_DUE_TO_GRAVITY = 9.81 # m/s^2 diff --git a/common/params_keys.h b/common/params_keys.h index 4776120c0..08f79bf5a 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -12,7 +12,7 @@ inline static std::unordered_map keys = { {"ApiCache_Device", {PERSISTENT, STRING}}, {"ApiCache_FirehoseStats", {PERSISTENT, JSON}}, {"AssistNowToken", {PERSISTENT, STRING}}, - {"AthenadPid", {PERSISTENT, STRING}}, + {"AthenadPid", {PERSISTENT, INT}}, {"AthenadUploadQueue", {PERSISTENT, JSON}}, {"AthenadRecentlyViewedRoutes", {PERSISTENT, STRING}}, {"BootCount", {PERSISTENT, INT}}, @@ -105,6 +105,7 @@ inline static std::unordered_map keys = { {"PandaSignatures", {CLEAR_ON_MANAGER_START, BYTES}}, {"PrimeType", {PERSISTENT, INT}}, {"RecordAudio", {PERSISTENT | BACKUP, BOOL}}, + {"RecordAudioFeedback", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RecordFront", {PERSISTENT | BACKUP, BOOL}}, {"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet {"SecOCKey", {PERSISTENT | DONT_LOG | BACKUP, STRING}}, @@ -177,7 +178,7 @@ inline static std::unordered_map keys = { {"SunnylinkCache_Roles", {PERSISTENT, STRING}}, {"SunnylinkCache_Users", {PERSISTENT, STRING}}, {"SunnylinkDongleId", {PERSISTENT, STRING}}, - {"SunnylinkdPid", {PERSISTENT, STRING}}, + {"SunnylinkdPid", {PERSISTENT, INT}}, {"SunnylinkEnabled", {PERSISTENT, BOOL}}, // Backup Manager params diff --git a/common/params_pyx.pyx b/common/params_pyx.pyx index 1a3e03e7f..bffa89b5d 100644 --- a/common/params_pyx.pyx +++ b/common/params_pyx.pyx @@ -103,14 +103,14 @@ cdef class Params: return cast(value) raise TypeError(f"Type mismatch while writing param {key}: {proposed_type=} {expected_type=} {value=}") - def cpp2python(self, t, value, default, key): + def _cpp2python(self, t, value, default, key): if value is None: return None try: return CPP_2_PYTHON[t](value) except (KeyError, TypeError, ValueError): cloudlog.warning(f"Failed to cast param {key} with {value=} from type {t=}") - return self.cpp2python(t, default, None, key) + return self._cpp2python(t, default, None, key) def get(self, key, bool block=False, bool return_default=False): cdef string k = self.check_key(key) @@ -127,8 +127,8 @@ cdef class Params: # it means we got an interrupt while waiting raise KeyboardInterrupt else: - return self.cpp2python(t, default_val, None, key) - return self.cpp2python(t, val, default_val, key) + return self._cpp2python(t, default_val, None, key) + return self._cpp2python(t, val, default_val, key) def get_bool(self, key, bool block=False): cdef string k = self.check_key(key) @@ -189,4 +189,9 @@ cdef class Params: cdef string k = self.check_key(key) cdef ParamKeyType t = self.p.getKeyType(k) cdef optional[string] default = self.p.getKeyDefaultValue(k) - return self.cpp2python(t, default.value(), None, key) if default.has_value() else None + return self._cpp2python(t, default.value(), None, key) if default.has_value() else None + + def cpp2python(self, key, value): + cdef string k = self.check_key(key) + cdef ParamKeyType t = self.p.getKeyType(k) + return self._cpp2python(t, value, None, key) diff --git a/common/prefix.py b/common/prefix.py index 762ae70fb..207f8477d 100644 --- a/common/prefix.py +++ b/common/prefix.py @@ -9,20 +9,19 @@ from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT class OpenpilotPrefix: - def __init__(self, prefix: str = None, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): + def __init__(self, prefix: str = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15]) self.msgq_path = os.path.join(Paths.shm_path(), self.prefix) + self.create_dirs_on_enter = create_dirs_on_enter self.clean_dirs_on_exit = clean_dirs_on_exit self.shared_download_cache = shared_download_cache def __enter__(self): self.original_prefix = os.environ.get('OPENPILOT_PREFIX', None) os.environ['OPENPILOT_PREFIX'] = self.prefix - try: - os.mkdir(self.msgq_path) - except FileExistsError: - pass - os.makedirs(Paths.log_root(), exist_ok=True) + + if self.create_dirs_on_enter: + self.create_dirs() if self.shared_download_cache: os.environ["COMMA_CACHE"] = DEFAULT_DOWNLOAD_CACHE_ROOT @@ -40,6 +39,13 @@ class OpenpilotPrefix: pass return False + def create_dirs(self): + try: + os.mkdir(self.msgq_path) + except FileExistsError: + pass + os.makedirs(Paths.log_root(), exist_ok=True) + def clean_dirs(self): symlink_path = Params().get_param_path() if os.path.exists(symlink_path): diff --git a/common/tests/test_params.py b/common/tests/test_params.py index 1f39769c2..592bf2c4b 100644 --- a/common/tests/test_params.py +++ b/common/tests/test_params.py @@ -37,9 +37,9 @@ class TestParams: def test_params_two_things(self): self.params.put("DongleId", "bob") - self.params.put("AthenadPid", "123") + self.params.put("AthenadPid", 123) assert self.params.get("DongleId") == "bob" - assert self.params.get("AthenadPid") == "123" + assert self.params.get("AthenadPid") == 123 def test_params_get_block(self): def _delayed_writer(): diff --git a/common/util.cc b/common/util.cc index 3d03c96bb..26a2bd60b 100644 --- a/common/util.cc +++ b/common/util.cc @@ -1,4 +1,5 @@ #include "common/util.h" +#include "common/swaglog.h" #include #include @@ -151,11 +152,16 @@ int safe_fflush(FILE *stream) { return ret; } -int safe_ioctl(int fd, unsigned long request, void *argp) { +int safe_ioctl(int fd, unsigned long request, void *argp, const char* exception_msg) { int ret; do { ret = ioctl(fd, request, argp); } while ((ret == -1) && (errno == EINTR)); + + if (ret == -1 && exception_msg) { + LOGE("safe_ioctl error: %s %s(%d) (fd: %d request: %lx argp: %p)", exception_msg, strerror(errno), errno, fd, request, argp); + throw std::runtime_error(exception_msg); + } return ret; } diff --git a/common/util.h b/common/util.h index 4fb4b13fa..f46db4d9f 100644 --- a/common/util.h +++ b/common/util.h @@ -88,7 +88,7 @@ int write_file(const char* path, const void* data, size_t size, int flags = O_WR FILE* safe_fopen(const char* filename, const char* mode); size_t safe_fwrite(const void * ptr, size_t size, size_t count, FILE * stream); int safe_fflush(FILE *stream); -int safe_ioctl(int fd, unsigned long request, void *argp); +int safe_ioctl(int fd, unsigned long request, void *argp, const char* exception_msg = nullptr); std::string readlink(const std::string& path); bool file_exists(const std::string& fn); diff --git a/docs/CARS.md b/docs/CARS.md index f2be4c5c9..a0a4fd6bd 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,7 +4,7 @@ A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# 313 Supported Cars +# 314 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -72,6 +72,7 @@ A supported vehicle is one that just works when you install a comma device. All |Genesis|GV80 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Accord 2023|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[5](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -179,7 +180,7 @@ A supported vehicle is one that just works when you install a comma device. All |Kia|Sportage Hybrid 2023[6](#footnotes)|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Kia|Stinger 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Telluride 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Telluride 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Lexus|ES 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| diff --git a/launch_env.sh b/launch_env.sh index b0bff4e57..0ed1395b3 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1 export VECLIB_MAXIMUM_THREADS=1 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="12.4" + export AGNOS_VERSION="12.6" fi export STAGING_ROOT="/data/safe_staging" diff --git a/opendbc_repo b/opendbc_repo index 5509df5eb..0ad154475 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 5509df5eb3e965335c04c61d4039e8378bf18718 +Subproject commit 0ad154475cebcaf99a4b471ea22f12e23b7c92d7 diff --git a/panda b/panda index e46680ff6..0e7a3fd8c 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit e46680ff6f8757a89ff82a5d835182725150b592 +Subproject commit 0e7a3fd8cf6f7a08b80ca15cea1b3989fc11b135 diff --git a/pyproject.toml b/pyproject.toml index b9e2d0ecb..0968cd4ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,7 +120,6 @@ dev = [ tools = [ "metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')", - #"rerun-sdk >= 0.18", # this is pretty big, so only enable once we use it ] [project.urls] @@ -152,6 +151,7 @@ markers = [ testpaths = [ "common", "selfdrive", + "system/manager", "system/updated", "system/athena", "system/camerad", diff --git a/release/README.md b/release/README.md index 06404be6a..0ceb76559 100644 --- a/release/README.md +++ b/release/README.md @@ -4,20 +4,24 @@ ## release checklist **Go to `devel-staging`** +- [ ] make issue to track release - [ ] update RELEASES.md -- [ ] update `devel-staging`: `git reset --hard origin/master-ci` +- [ ] trigger new nightly build: https://github.com/commaai/openpilot/actions/workflows/release.yaml +- [ ] update `devel-staging`: `git reset --hard origin/__nightly` +- [ ] build new userdata partition from `release3-staging` - [ ] open a pull request from `devel-staging` to `devel` -- [ ] post on Discord +- [ ] post on Discord, tag `@release crew` **Go to `devel`** - [ ] bump version on master: `common/version.h` and `RELEASES.md` -- [ ] before merging the pull request +- [ ] before merging the pull request, test the following: - [ ] update from previous release -> new release - [ ] update from new release -> previous release - [ ] fresh install with `openpilot-test.comma.ai` - [ ] drive on fresh install - [ ] no submodules or LFS - [ ] check sentry, MTBF, etc. + - [ ] stress test in production **Go to `release3`** - [ ] publish the blog post diff --git a/selfdrive/car/car_specific.py b/selfdrive/car/car_specific.py index 083885211..270111524 100644 --- a/selfdrive/car/car_specific.py +++ b/selfdrive/car/car_specific.py @@ -57,7 +57,7 @@ class CarSpecificEvents: events.add(EventName.belowSteerSpeed) elif self.CP.brand == 'honda': - events = self.create_common_events(CS, CS_prev, pcm_enable=False) + events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport], pcm_enable=False) if self.CP.pcmCruise and CS.vEgo < self.CP.minEnableSpeed: events.add(EventName.belowEngageSpeed) diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index 01ef2d7c9..5ffb43f9e 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -2,7 +2,7 @@ import math import numpy as np from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.car.cruise_ext import VCruiseHelperSP diff --git a/selfdrive/car/tests/test_cruise_speed.py b/selfdrive/car/tests/test_cruise_speed.py index dd78b7768..4f9444d4b 100644 --- a/selfdrive/car/tests/test_cruise_speed.py +++ b/selfdrive/car/tests/test_cruise_speed.py @@ -6,7 +6,7 @@ from parameterized import parameterized_class from cereal import log from openpilot.selfdrive.car.cruise import VCruiseHelper, V_CRUISE_MIN, V_CRUISE_MAX, V_CRUISE_INITIAL, IMPERIAL_INCREMENT from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver ButtonEvent = car.CarState.ButtonEvent diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 4f61ad3c3..033370c92 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -439,7 +439,9 @@ class TestCarModelBase(unittest.TestCase): if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.brake > 0.05: brake_pressed = False checks['brakePressed'] += brake_pressed != self.safety.get_brake_pressed_prev() - checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev() + # TODO: remove this exception before closing commaai/opendbc#1100 + if not (self.CP.brand == "honda" and self.CP.flags & HondaFlags.BOSCH): + checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev() checks['steeringDisengage'] += CS.steeringDisengage != self.safety.get_steering_disengage_prev() if self.CP.pcmCruise: diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 360c1a473..2b4b62c8e 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -2,11 +2,11 @@ import math import threading import time -from typing import SupportsFloat +from numbers import Number from cereal import car, log import cereal.messaging as messaging -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper from openpilot.common.swaglog import cloudlog @@ -147,7 +147,7 @@ class Controls(ControlsExt, ModelStateBase): # Ensure no NaNs/Infs for p in ACTUATOR_FIELDS: attr = getattr(actuators, p) - if not isinstance(attr, SupportsFloat): + if not isinstance(attr, Number): continue if not math.isfinite(attr): diff --git a/selfdrive/controls/lib/desire_helper.py b/selfdrive/controls/lib/desire_helper.py index 23386e2ad..2adfa65f6 100644 --- a/selfdrive/controls/lib/desire_helper.py +++ b/selfdrive/controls/lib/desire_helper.py @@ -1,5 +1,5 @@ from cereal import log -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.realtime import DT_MDL from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode diff --git a/selfdrive/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py index 51eb694b4..e28fa3021 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -1,5 +1,5 @@ import numpy as np -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.common.realtime import DT_CTRL, DT_MDL MIN_SPEED = 1.0 diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index d7efcbcd8..7885902f1 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -2,9 +2,9 @@ import math import numpy as np from cereal import log -from opendbc.car import FRICTION_THRESHOLD, get_friction +from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction from opendbc.car.interfaces import LatControlInputs -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.common.pid import PIDController diff --git a/selfdrive/controls/lib/ldw.py b/selfdrive/controls/lib/ldw.py index caf03fec7..78a6d6cf6 100644 --- a/selfdrive/controls/lib/ldw.py +++ b/selfdrive/controls/lib/ldw.py @@ -1,6 +1,6 @@ from cereal import log from openpilot.common.realtime import DT_CTRL -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV CAMERA_OFFSET = 0.04 diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 46aec8e61..785a97a7a 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -4,7 +4,7 @@ import numpy as np import cereal.messaging as messaging from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.modeld.constants import ModelConstants @@ -21,7 +21,7 @@ LON_MPC_STEP = 0.2 # first step is 0.2s A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6] A_CRUISE_MAX_BP = [0., 10.0, 25., 40.] CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N] -ALLOW_THROTTLE_THRESHOLD = 0.5 +ALLOW_THROTTLE_THRESHOLD = 0.4 MIN_ALLOW_THROTTLE_SPEED = 2.5 # Lookup table for turns @@ -188,7 +188,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): def publish(self, sm, pm): plan_send = messaging.new_message('longitudinalPlan') - plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState']) + plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'radarState']) longitudinalPlan = plan_send.longitudinalPlan longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2'] diff --git a/selfdrive/debug/run_process_on_route.py b/selfdrive/debug/run_process_on_route.py index 2ccb0fb3e..d6743fedc 100755 --- a/selfdrive/debug/run_process_on_route.py +++ b/selfdrive/debug/run_process_on_route.py @@ -1,23 +1,25 @@ #!/usr/bin/env python3 - import argparse from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, replay_process +from openpilot.selfdrive.test.process_replay.test_processes import EXCLUDED_PROCS from openpilot.tools.lib.logreader import LogReader, save_log +ALLOW_PROCS = {c.proc_name for c in CONFIGS} + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Run process on route and create new logs", formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--fingerprint", help="The fingerprint to use") parser.add_argument("route", help="The route name to use") - parser.add_argument("process", nargs='+', help="The process(s) to run") + parser.add_argument("--fingerprint", help="The fingerprint to use") + parser.add_argument("--whitelist-procs", nargs='*', default=ALLOW_PROCS, help="Whitelist given processes (e.g. controlsd)") + parser.add_argument("--blacklist-procs", nargs='*', default=EXCLUDED_PROCS, help="Blacklist given processes (e.g. controlsd)") args = parser.parse_args() - cfgs = [c for c in CONFIGS if c.proc_name in args.process] - - lr = LogReader(args.route) - inputs = list(lr) + allowed_procs = set(args.whitelist_procs) - set(args.blacklist_procs) + cfgs = [c for c in CONFIGS if c.proc_name in allowed_procs] + inputs = list(LogReader(args.route)) outputs = replay_process(cfgs, inputs, fingerprint=args.fingerprint) # Remove message generated by the process under test and merge in the new messages @@ -25,6 +27,6 @@ if __name__ == "__main__": inputs = [i for i in inputs if i.which() not in produces] outputs = sorted(inputs + outputs, key=lambda x: x.logMonoTime) - fn = f"{args.route.replace('/', '_')}_{'_'.join(args.process)}.zst" + fn = f"{args.route.replace('/', '_')}_{'_'.join(allowed_procs)}.zst" print(f"Saving log to {fn}") save_log(fn, outputs) diff --git a/selfdrive/locationd/calibrationd.py b/selfdrive/locationd/calibrationd.py index 59f30dbf7..03c044982 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/selfdrive/locationd/calibrationd.py @@ -14,7 +14,7 @@ from typing import NoReturn from cereal import log, car import cereal.messaging as messaging from openpilot.system.hardware import HARDWARE -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process from openpilot.common.transformations.orientation import rot_from_euler, euler_from_rot diff --git a/selfdrive/locationd/models/car_kf.py b/selfdrive/locationd/models/car_kf.py index 6db749b94..27cc4ef9c 100755 --- a/selfdrive/locationd/models/car_kf.py +++ b/selfdrive/locationd/models/car_kf.py @@ -5,7 +5,7 @@ from typing import Any import numpy as np -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.selfdrive.locationd.models.constants import ObservationKind from openpilot.common.swaglog import cloudlog diff --git a/selfdrive/locationd/torqued.py b/selfdrive/locationd/torqued.py index 130a352c5..e0409957f 100755 --- a/selfdrive/locationd/torqued.py +++ b/selfdrive/locationd/torqued.py @@ -4,7 +4,7 @@ from collections import deque, defaultdict import cereal.messaging as messaging from cereal import car, log -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, DT_MDL from openpilot.common.filter_simple import FirstOrderFilter @@ -33,7 +33,7 @@ MIN_BUCKET_POINTS = np.array([100, 300, 500, 500, 500, 500, 300, 100]) MIN_ENGAGE_BUFFER = 2 # secs VERSION = 1 # bump this to invalidate old parameter caches -ALLOWED_CARS = ['toyota', 'hyundai', 'rivian'] +ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda'] def slope2rot(slope): diff --git a/selfdrive/pandad/spi.cc b/selfdrive/pandad/spi.cc index 108b11b9d..b6ee57801 100644 --- a/selfdrive/pandad/spi.cc +++ b/selfdrive/pandad/spi.cc @@ -66,58 +66,44 @@ PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) { // 50MHz is the max of the 845. note that some older // revs of the comma three may not support this speed uint32_t spi_speed = 50000000; - - if (!util::file_exists(SPI_DEVICE)) { - goto fail; - } - - spi_fd = open(SPI_DEVICE.c_str(), O_RDWR); - if (spi_fd < 0) { - LOGE("failed opening SPI device %d", spi_fd); - goto fail; - } - - // SPI settings - ret = util::safe_ioctl(spi_fd, SPI_IOC_WR_MODE, &spi_mode); - if (ret < 0) { - LOGE("failed setting SPI mode %d", ret); - goto fail; - } - - ret = util::safe_ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &spi_speed); - if (ret < 0) { - LOGE("failed setting SPI speed"); - goto fail; - } - - ret = util::safe_ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bits_per_word); - if (ret < 0) { - LOGE("failed setting SPI bits per word"); - goto fail; - } - - // get hw UID/serial - ret = control_read(0xc3, 0, 0, uid, uid_len, 100); - if (ret == uid_len) { - std::stringstream stream; - for (int i = 0; i < uid_len; i++) { - stream << std::hex << std::setw(2) << std::setfill('0') << int(uid[i]); + try { + if (!util::file_exists(SPI_DEVICE)) { + throw std::runtime_error("Error connecting to panda: SPI device not found"); } - hw_serial = stream.str(); - } else { - LOGD("failed to get serial %d", ret); - goto fail; - } - if (!serial.empty() && (serial != hw_serial)) { - goto fail; - } + spi_fd = open(SPI_DEVICE.c_str(), O_RDWR); + if (spi_fd < 0) { + LOGE("failed opening SPI device %d", spi_fd); + throw std::runtime_error("Error connecting to panda: failed to open SPI device"); + } + // SPI settings + util::safe_ioctl(spi_fd, SPI_IOC_WR_MODE, &spi_mode, "failed setting SPI mode"); + util::safe_ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &spi_speed, "failed setting SPI speed"); + util::safe_ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bits_per_word, "failed setting SPI bits per word"); + + // get hw UID/serial + ret = control_read(0xc3, 0, 0, uid, uid_len, 100); + if (ret == uid_len) { + std::stringstream stream; + for (int i = 0; i < uid_len; i++) { + stream << std::hex << std::setw(2) << std::setfill('0') << int(uid[i]); + } + hw_serial = stream.str(); + } else { + LOGD("failed to get serial %d", ret); + throw std::runtime_error("Error connecting to panda: failed to get serial"); + } + + if (!serial.empty() && (serial != hw_serial)) { + throw std::runtime_error("Error connecting to panda: serial mismatch"); + } + + } catch (...) { + cleanup(); + throw; + } return; - -fail: - cleanup(); - throw std::runtime_error("Error connecting to panda"); } PandaSpiHandle::~PandaSpiHandle() { diff --git a/selfdrive/selfdrived/alerts_offroad.json b/selfdrive/selfdrived/alerts_offroad.json index 15dd06412..183c1f854 100644 --- a/selfdrive/selfdrived/alerts_offroad.json +++ b/selfdrive/selfdrived/alerts_offroad.json @@ -46,7 +46,7 @@ "severity": 0 }, "Offroad_ExcessiveActuation": { - "text": "openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support.", + "text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.", "severity": 1, "_comment": "Set extra field to lateral or longitudinal." } diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 17e170010..591e54918 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -4,10 +4,12 @@ import os from cereal import log, car import cereal.messaging as messaging -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.git import get_short_branch from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER +from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER +from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ NoEntryAlert, SoftDisableAlert, UserSoftDisableAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, \ @@ -40,6 +42,7 @@ class Events(EventsBase): return log.OnroadEvent + # ********** helper functions ********** def get_display_speed(speed_ms: float, metric: bool) -> str: speed = int(round(speed_ms * (CV.MS_TO_KPH if metric else CV.MS_TO_MPH))) @@ -92,6 +95,14 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2) +def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE) + return NormalPermanentAlert( + "Recording Audio Feedback", + f"{round(duration)} second{'s' if round(duration) != 1 else ''} remaining. Press again to save early.", + priority=Priority.LOW) + + # *** debug alerts *** def out_of_space_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: @@ -203,6 +214,7 @@ def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messagin return NormalPermanentAlert("Invalid LKAS setting", text) + EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { # ********** events with no alerts ********** @@ -824,9 +836,13 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { ET.WARNING: personality_changed_alert, }, - EventName.userFlag: { + EventName.userBookmark: { ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5), }, + + EventName.audioFeedback: { + ET.PERMANENT: audio_feedback_alert, + }, } diff --git a/selfdrive/selfdrived/helpers.py b/selfdrive/selfdrived/helpers.py new file mode 100644 index 000000000..f7468cbe4 --- /dev/null +++ b/selfdrive/selfdrived/helpers.py @@ -0,0 +1,54 @@ +import math +from enum import StrEnum, auto + +from cereal import car, messaging +from openpilot.common.realtime import DT_CTRL +from openpilot.selfdrive.locationd.helpers import Pose +from opendbc.car import ACCELERATION_DUE_TO_GRAVITY +from opendbc.car.lateral import ISO_LATERAL_ACCEL +from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX + +MIN_EXCESSIVE_ACTUATION_COUNT = int(0.25 / DT_CTRL) +MIN_LATERAL_ENGAGE_BUFFER = int(1 / DT_CTRL) + + +class ExcessiveActuationType(StrEnum): + LONGITUDINAL = auto() + LATERAL = auto() + + +class ExcessiveActuationCheck: + def __init__(self): + self._excessive_counter = 0 + self._engaged_counter = 0 + + def update(self, sm: messaging.SubMaster, CS: car.CarState, calibrated_pose: Pose) -> ExcessiveActuationType | None: + # CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc. + # longitudinal + accel_calibrated = calibrated_pose.acceleration.x + excessive_long_actuation = sm['carControl'].longActive and (accel_calibrated > ACCEL_MAX * 2 or accel_calibrated < ACCEL_MIN * 2) + + # lateral + yaw_rate = calibrated_pose.angular_velocity.yaw + roll = sm['liveParameters'].roll + roll_compensated_lateral_accel = (CS.vEgo * yaw_rate) - (math.sin(roll) * ACCELERATION_DUE_TO_GRAVITY) + + # Prevent false positives after overriding + excessive_lat_actuation = False + self._engaged_counter = self._engaged_counter + 1 if sm['carControl'].latActive and not CS.steeringPressed else 0 + if self._engaged_counter > MIN_LATERAL_ENGAGE_BUFFER: + if abs(roll_compensated_lateral_accel) > ISO_LATERAL_ACCEL * 2: + excessive_lat_actuation = True + + # livePose acceleration can be noisy due to bad mounting or aliased livePose measurements + livepose_valid = abs(CS.aEgo - accel_calibrated) < 2 + self._excessive_counter = self._excessive_counter + 1 if livepose_valid and (excessive_long_actuation or excessive_lat_actuation) else 0 + + excessive_type = None + if self._excessive_counter > MIN_EXCESSIVE_ACTUATION_COUNT: + if excessive_long_actuation: + excessive_type = ExcessiveActuationType.LONGITUDINAL + else: + excessive_type = ExcessiveActuationType.LATERAL + + return excessive_type diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index e20327057..342c78b8d 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -7,7 +7,6 @@ import cereal.messaging as messaging from cereal import car, log, custom from msgq.visionipc import VisionIpcClient, VisionStreamType -from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX from openpilot.common.params import Params @@ -18,6 +17,7 @@ from openpilot.common.gps import get_gps_location_service from openpilot.selfdrive.car.car_specific import CarSpecificEvents from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose from openpilot.selfdrive.selfdrived.events import Events, ET +from openpilot.selfdrive.selfdrived.helpers import ExcessiveActuationCheck from openpilot.selfdrive.selfdrived.state import StateMachine from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert @@ -34,7 +34,6 @@ SIMULATION = "SIMULATION" in os.environ TESTING_CLOSET = "TESTING_CLOSET" in os.environ LONGITUDINAL_PERSONALITY_MAP = {v: k for k, v in log.LongitudinalPersonality.schema.enumerants.items()} -MIN_EXCESSIVE_ACTUATION_COUNT = int(0.25 / DT_CTRL) ThermalStatus = log.DeviceState.ThermalStatus State = log.SelfdriveState.OpenpilotState @@ -48,21 +47,6 @@ SafetyModel = car.CarParams.SafetyModel IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput) -def check_excessive_actuation(sm: messaging.SubMaster, CS: car.CarState, calibrator: PoseCalibrator, counter: int) -> tuple[int, bool]: - # CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc. - device_pose = Pose.from_live_pose(sm['livePose']) - calibrated_pose = calibrator.build_calibrated_pose(device_pose) - accel_calibrated = calibrated_pose.acceleration.x - - # livePose acceleration can be noisy due to bad mounting or aliased livePose measurements - accel_valid = abs(CS.aEgo - accel_calibrated) < 2 - - excessive_actuation = accel_calibrated > ACCEL_MAX * 2 or accel_calibrated < ACCEL_MIN * 2 - counter = counter + 1 if sm['carControl'].longActive and excessive_actuation and accel_valid else 0 - - return counter, counter > MIN_EXCESSIVE_ACTUATION_COUNT - - class SelfdriveD(CruiseHelper): def __init__(self, CP=None, CP_SP=None): self.params = Params() @@ -85,7 +69,11 @@ class SelfdriveD(CruiseHelper): self.CP_SP = CP_SP self.car_events = CarSpecificEvents(self.CP) - self.calibrator = PoseCalibrator() + + self.pose_calibrator = PoseCalibrator() + self.calibrated_pose: Pose | None = None + self.excessive_actuation_check = ExcessiveActuationCheck() + self.excessive_actuation = self.params.get("Offroad_ExcessiveActuation") is not None # Setup sockets self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'] + ['selfdriveStateSP', 'onroadEventsSP']) @@ -107,7 +95,7 @@ class SelfdriveD(CruiseHelper): self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', 'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay', 'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters', - 'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userFlag'] + \ + 'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback'] + \ self.camera_packets + self.sensor_packets + self.gps_packets, ignore_alive=ignore, ignore_avg_freq=ignore, ignore_valid=ignore, frequency=int(1/DT_CTRL)) @@ -143,8 +131,6 @@ class SelfdriveD(CruiseHelper): self.experimental_mode = False self.personality = self.params.get("LongitudinalPersonality", return_default=True) self.recalibrating_seen = False - self.excessive_actuation = self.params.get("Offroad_ExcessiveActuation") is not None - self.excessive_actuation_counter = 0 self.state_machine = StateMachine() self.rk = Ratekeeper(100, print_delay_threshold=None) @@ -205,9 +191,12 @@ class SelfdriveD(CruiseHelper): self.events.add(EventName.selfdriveInitializing) return - # Check for user flag (bookmark) press - if self.sm.updated['userFlag']: - self.events.add(EventName.userFlag) + # Check for user bookmark press (bookmark button or end of LKAS button feedback) + if self.sm.updated['userBookmark']: + self.events.add(EventName.userBookmark) + + if self.sm.updated['audioFeedback']: + self.events.add(EventName.audioFeedback) # Don't add any more events while in dashcam mode if self.CP.passive: @@ -276,14 +265,18 @@ class SelfdriveD(CruiseHelper): if self.sm['driverAssistance'].leftLaneDeparture or self.sm['driverAssistance'].rightLaneDeparture: self.events.add(EventName.ldw) - # Check for excessive (longitudinal) actuation + # Check for excessive actuation if self.sm.updated['liveCalibration']: - self.calibrator.feed_live_calib(self.sm['liveCalibration']) + self.pose_calibrator.feed_live_calib(self.sm['liveCalibration']) + if self.sm.updated['livePose']: + device_pose = Pose.from_live_pose(self.sm['livePose']) + self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_pose) - self.excessive_actuation_counter, excessive_actuation = check_excessive_actuation(self.sm, CS, self.calibrator, self.excessive_actuation_counter) - if not self.excessive_actuation and excessive_actuation: - set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text="longitudinal") - self.excessive_actuation = True + if self.calibrated_pose is not None: + excessive_actuation = self.excessive_actuation_check.update(self.sm, CS, self.calibrated_pose) + if not self.excessive_actuation and excessive_actuation is not None: + set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text=str(excessive_actuation)) + self.excessive_actuation = True if self.excessive_actuation: self.events.add(EventName.excessiveActuation) @@ -339,13 +332,12 @@ class SelfdriveD(CruiseHelper): self.events.add(EventName.cameraFrameRate) if not REPLAY and self.rk.lagging: self.events.add(EventName.selfdrivedLagging) - if not self.sm.valid['radarState']: - if self.sm['radarState'].radarErrors.canError: - self.events.add(EventName.canError) - elif self.sm['radarState'].radarErrors.radarUnavailableTemporary: - self.events.add(EventName.radarTempUnavailable) - else: - self.events.add(EventName.radarFault) + if self.sm['radarState'].radarErrors.canError: + self.events.add(EventName.canError) + elif self.sm['radarState'].radarErrors.radarUnavailableTemporary: + self.events.add(EventName.radarTempUnavailable) + elif any(self.sm['radarState'].radarErrors.to_dict().values()): + self.events.add(EventName.radarFault) if not self.sm.valid['pandaStates']: self.events.add(EventName.usbError) if CS.canTimeout: diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index bc5ccf784..b1190c25e 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -4,8 +4,9 @@ import time import copy import heapq import signal -from collections import Counter, OrderedDict +from collections import Counter from dataclasses import dataclass, field +from itertools import islice from typing import Any from collections.abc import Callable, Iterable from tqdm import tqdm @@ -16,12 +17,13 @@ import cereal.messaging as messaging from cereal import car from cereal.services import SERVICE_LIST from msgq.visionipc import VisionIpcServer, get_endpoint_name as vipc_get_endpoint_name +from opendbc.car.can_definitions import CanData from opendbc.car.car_helpers import get_car, interfaces from openpilot.common.params import Params from openpilot.common.prefix import OpenpilotPrefix from openpilot.common.timeout import Timeout from openpilot.common.realtime import DT_CTRL -from openpilot.selfdrive.car.card import can_comm_callbacks, convert_to_capnp +from openpilot.selfdrive.car.card import convert_to_capnp from openpilot.system.manager.process_config import managed_processes from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state, available_streams from openpilot.selfdrive.test.process_replay.migration import migrate_all @@ -30,22 +32,10 @@ from openpilot.tools.lib.logreader import LogIterable from openpilot.tools.lib.framereader import FrameReader # Numpy gives different results based on CPU features after version 19 -NUMPY_TOLERANCE = 1e-7 +NUMPY_TOLERANCE = 1e-2 PROC_REPLAY_DIR = os.path.dirname(os.path.abspath(__file__)) FAKEDATA = os.path.join(PROC_REPLAY_DIR, "fakedata/") -class DummySocket: - def __init__(self): - self.data: list[bytes] = [] - - def receive(self, non_blocking: bool = False) -> bytes | None: - if non_blocking: - return None - - return self.data.pop() - - def send(self, data: bytes): - self.data.append(data) class LauncherWithCapture: def __init__(self, capture: ProcessOutputCapture, launcher: Callable): @@ -63,8 +53,7 @@ class ReplayContext: self.pubs = cfg.pubs self.main_pub = cfg.main_pub self.main_pub_drained = cfg.main_pub_drained - self.unlocked_pubs = cfg.unlocked_pubs - assert(len(self.pubs) != 0 or self.main_pub is not None) + assert len(self.pubs) != 0 or self.main_pub is not None def __enter__(self): self.open_context() @@ -79,9 +68,8 @@ class ReplayContext: messaging.set_fake_prefix(self.proc_name) if self.main_pub is None: - self.events = OrderedDict() - pubs_with_events = [pub for pub in self.pubs if pub not in self.unlocked_pubs] - for pub in pubs_with_events: + self.events = {} + for pub in self.pubs: self.events[pub] = messaging.fake_event_handle(pub, enable=True) else: self.events = {self.main_pub: messaging.fake_event_handle(self.main_pub, enable=True)} @@ -138,16 +126,21 @@ class ProcessConfig: processing_time: float = 0.001 timeout: int = 30 simulation: bool = True + # Set to service process receives on first main_pub: str | None = None - main_pub_drained: bool = True + main_pub_drained: bool = False vision_pubs: list[str] = field(default_factory=list) ignore_alive_pubs: list[str] = field(default_factory=list) - unlocked_pubs: list[str] = field(default_factory=list) + + def __post_init__(self): + # If the process is polling a service, we can just lock that one to speed up replay + if self.main_pub is None and isinstance(self.should_recv_callback, MessageBasedRcvCallback): + self.main_pub = self.should_recv_callback.trigger_msg_type class ProcessContainer: def __init__(self, cfg: ProcessConfig): - self.prefix = OpenpilotPrefix(clean_dirs_on_exit=False) + self.prefix = OpenpilotPrefix(create_dirs_on_enter=False, clean_dirs_on_exit=False) self.cfg = copy.deepcopy(cfg) self.process = copy.deepcopy(managed_processes[cfg.proc_name]) self.msg_queue: list[capnp._DynamicStructReader] = [] @@ -229,6 +222,7 @@ class ProcessContainer: fingerprint: str | None, capture_output: bool ): with self.prefix as p: + self.prefix.create_dirs() self._setup_env(params_config, environ_config) if self.cfg.config_callback is not None: @@ -254,11 +248,6 @@ class ProcessContainer: if self.cfg.init_callback is not None: self.cfg.init_callback(self.rc, self.pm, all_msgs, fingerprint) - # wait for process to startup - with Timeout(10, error_msg=f"timed out waiting for process to start: {repr(self.cfg.proc_name)}"): - while not all(self.pm.all_readers_updated(s) for s in self.cfg.pubs if s not in self.cfg.ignore_alive_pubs): - time.sleep(0) - def stop(self): with self.prefix: self.process.signal(signal.SIGKILL) @@ -267,28 +256,42 @@ class ProcessContainer: self.prefix.clean_dirs() self._clean_env() + def get_output_msgs(self, start_time: int): + assert self.rc and self.sockets + + output_msgs = [] + self.rc.wait_for_recv_called() + for socket in self.sockets: + ms = messaging.drain_sock(socket) + for m in ms: + m = m.as_builder() + m.logMonoTime = start_time + int(self.cfg.processing_time * 1e9) + output_msgs.append(m.as_reader()) + return output_msgs + def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, FrameReader] | None) -> list[capnp._DynamicStructReader]: assert self.rc and self.pm and self.sockets and self.process.proc output_msgs = [] - with self.prefix, Timeout(self.cfg.timeout, error_msg=f"timed out testing process {repr(self.cfg.proc_name)}"): - end_of_cycle = True - if self.cfg.should_recv_callback is not None: - end_of_cycle = self.cfg.should_recv_callback(msg, self.cfg, self.cnt) - - self.msg_queue.append(msg) - if end_of_cycle: - self.rc.wait_for_recv_called() + end_of_cycle = True + if self.cfg.should_recv_callback is not None: + end_of_cycle = self.cfg.should_recv_callback(msg, self.cfg, self.cnt) + self.msg_queue.append(msg) + if end_of_cycle: + with self.prefix, Timeout(self.cfg.timeout, error_msg=f"timed out testing process {repr(self.cfg.proc_name)}"): # call recv to let sub-sockets reconnect, after we know the process is ready if self.cnt == 0: for s in self.sockets: messaging.recv_one_or_none(s) - # empty recv on drained pub indicates the end of messages, only do that if there're any + # certain processes use drain_sock. need to cause empty recv to break from this loop trigger_empty_recv = False if self.cfg.main_pub and self.cfg.main_pub_drained: - trigger_empty_recv = next((True for m in self.msg_queue if m.which() == self.cfg.main_pub), False) + trigger_empty_recv = any(m.which() == self.cfg.main_pub for m in self.msg_queue) + + # get output msgs from previous inputs + output_msgs = self.get_output_msgs(msg.logMonoTime) for m in self.msg_queue: self.pm.send(m.which(), m.as_builder()) @@ -303,14 +306,8 @@ class ProcessContainer: self.msg_queue = [] self.rc.unlock_sockets() - self.rc.wait_for_next_recv(trigger_empty_recv) - - for socket in self.sockets: - ms = messaging.drain_sock(socket) - for m in ms: - m = m.as_builder() - m.logMonoTime = msg.logMonoTime + int(self.cfg.processing_time * 1e9) - output_msgs.append(m.as_reader()) + if trigger_empty_recv: + self.rc.unlock_sockets() self.cnt += 1 assert self.process.proc.is_alive() @@ -320,7 +317,7 @@ class ProcessContainer: def card_fingerprint_callback(rc, pm, msgs, fingerprint): print("start fingerprinting") params = Params() - canmsgs = [msg for msg in msgs if msg.which() == "can"][:300] + canmsgs = list(islice((m for m in msgs if m.which() == "can"), 300)) # card expects one arbitrary can and pandaState rc.send_sync(pm, "can", messaging.new_message("can", 1)) @@ -345,36 +342,27 @@ def get_car_params_callback(rc, pm, msgs, fingerprint): CP = CarInterface.get_non_essential_params(fingerprint) CP_SP = CarInterface.get_non_essential_params_sp(CP, fingerprint) else: - can = DummySocket() - sendcan = DummySocket() - - canmsgs = [msg for msg in msgs if msg.which() == "can"] + can_msgs = ([CanData(can.address, can.dat, can.src) for can in m.can] for m in msgs if m.which() == "can") cached_params_raw = params.get("CarParamsCache") - has_cached_cp = cached_params_raw is not None - assert len(canmsgs) != 0, "CAN messages are required for fingerprinting" - assert os.environ.get("SKIP_FW_QUERY", False) or has_cached_cp, \ + assert next(can_msgs, None), "CAN messages are required for fingerprinting" + assert os.environ.get("SKIP_FW_QUERY", False) or cached_params_raw is not None, \ "CarParamsCache is required for fingerprinting. Make sure to keep carParams msgs in the logs." - for m in canmsgs[:300]: - can.send(m.as_builder().to_bytes()) - can_callbacks = can_comm_callbacks(can, sendcan) + def can_recv(wait_for_one: bool = False) -> list[list[CanData]]: + return [next(can_msgs, [])] cached_params = None - if has_cached_cp: + if cached_params_raw is not None: with car.CarParams.from_bytes(cached_params_raw) as _cached_params: cached_params = _cached_params - _CI = get_car(*can_callbacks, lambda obd: None, Params().get_bool("AlphaLongitudinalEnabled"), False, cached_params=cached_params) + _CI = get_car(can_recv, lambda _msgs: None, lambda obd: None, params.get_bool("AlphaLongitudinalEnabled"), False, cached_params=cached_params) CP, CP_SP = _CI.CP, _CI.CP_SP params.put("CarParams", CP.to_bytes()) params.put("CarParamsSP", convert_to_capnp(CP_SP).to_bytes()) -def selfdrived_rcv_callback(msg, cfg, frame): - return (frame - 1) == 0 or msg.which() == 'carState' - - def card_rcv_callback(msg, cfg, frame): # no sendcan until card is initialized if msg.which() != "can": @@ -389,21 +377,6 @@ def card_rcv_callback(msg, cfg, frame): return len(socks) > 0 -def calibration_rcv_callback(msg, cfg, frame): - # calibrationd publishes 1 calibrationData every 5 cameraOdometry packets. - # should_recv always true to increment frame - return (frame - 1) == 0 or msg.which() == 'cameraOdometry' - - -def torqued_rcv_callback(msg, cfg, frame): - # should_recv always true to increment frame - return (frame - 1) == 0 or msg.which() == 'livePose' - - -def dmonitoringmodeld_rcv_callback(msg, cfg, frame): - return msg.which() == "driverCameraState" - - class ModeldCameraSyncRcvCallback: def __init__(self): self.road_present = False @@ -428,26 +401,13 @@ class ModeldCameraSyncRcvCallback: class MessageBasedRcvCallback: - def __init__(self, trigger_msg_type): + def __init__(self, trigger_msg_type: str, first_frame: bool = False): self.trigger_msg_type = trigger_msg_type + self.first_frame = first_frame def __call__(self, msg, cfg, frame): - return msg.which() == self.trigger_msg_type - - -class FrequencyBasedRcvCallback: - def __init__(self, trigger_msg_type): - self.trigger_msg_type = trigger_msg_type - - def __call__(self, msg, cfg, frame): - if msg.which() != self.trigger_msg_type: - return False - - resp_sockets = [ - s for s in cfg.subs - if frame % max(1, int(SERVICE_LIST[msg.which()].frequency / SERVICE_LIST[s].frequency)) == 0 - ] - return bool(len(resp_sockets)) + # publish on first frame or trigger msg + return ((frame - 1) == 0 and self.first_frame) or msg.which() == self.trigger_msg_type def selfdrived_config_callback(params, cfg, lr): @@ -462,16 +422,16 @@ CONFIGS = [ proc_name="selfdrived", pubs=[ "carState", "deviceState", "pandaStates", "peripheralState", "liveCalibration", "driverMonitoringState", - "longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", - "modelV2", "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", - "liveTorqueParameters", "accelerometer", "gyroscope", "carOutput", - "gpsLocationExternal", "gpsLocation", "controlsState", "carControl", "driverAssistance", "alertDebug", + "longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", "modelV2", + "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", "liveTorqueParameters", + "accelerometer", "gyroscope", "carOutput", "gpsLocationExternal", "gpsLocation", "controlsState", + "carControl", "driverAssistance", "alertDebug", "audioFeedback", ], subs=["selfdriveState", "onroadEvents"], ignore=["logMonoTime"], config_callback=selfdrived_config_callback, init_callback=get_car_params_callback, - should_recv_callback=selfdrived_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("carState", True), tolerance=NUMPY_TOLERANCE, processing_time=0.004, ), @@ -496,6 +456,7 @@ CONFIGS = [ tolerance=NUMPY_TOLERANCE, processing_time=0.004, main_pub="can", + main_pub_drained=True, ), ProcessConfig( proc_name="radard", @@ -503,7 +464,7 @@ CONFIGS = [ subs=["radarState"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=FrequencyBasedRcvCallback("modelV2"), + should_recv_callback=MessageBasedRcvCallback("modelV2"), ), ProcessConfig( proc_name="plannerd", @@ -511,7 +472,7 @@ CONFIGS = [ subs=["longitudinalPlan", "driverAssistance"], ignore=["logMonoTime", "longitudinalPlan.processingDelay", "longitudinalPlan.solverExecutionTime"], init_callback=get_car_params_callback, - should_recv_callback=FrequencyBasedRcvCallback("modelV2"), + should_recv_callback=MessageBasedRcvCallback("modelV2"), tolerance=NUMPY_TOLERANCE, ), ProcessConfig( @@ -520,14 +481,14 @@ CONFIGS = [ subs=["liveCalibration"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=calibration_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("cameraOdometry", True), ), ProcessConfig( proc_name="dmonitoringd", pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "selfdriveState"], subs=["driverMonitoringState"], ignore=["logMonoTime"], - should_recv_callback=FrequencyBasedRcvCallback("driverStateV2"), + should_recv_callback=MessageBasedRcvCallback("driverStateV2"), tolerance=NUMPY_TOLERANCE, ), ProcessConfig( @@ -539,7 +500,6 @@ CONFIGS = [ ignore=["logMonoTime"], should_recv_callback=MessageBasedRcvCallback("cameraOdometry"), tolerance=NUMPY_TOLERANCE, - unlocked_pubs=["accelerometer", "gyroscope"], ), ProcessConfig( proc_name="paramsd", @@ -547,7 +507,7 @@ CONFIGS = [ subs=["liveParameters"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=FrequencyBasedRcvCallback("livePose"), + should_recv_callback=MessageBasedRcvCallback("livePose"), tolerance=NUMPY_TOLERANCE, processing_time=0.004, ), @@ -572,7 +532,7 @@ CONFIGS = [ subs=["liveTorqueParameters"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=torqued_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("livePose", True), tolerance=NUMPY_TOLERANCE, ), ProcessConfig( @@ -584,7 +544,6 @@ CONFIGS = [ tolerance=NUMPY_TOLERANCE, processing_time=0.020, main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("roadCameraState").stream), - main_pub_drained=False, vision_pubs=["roadCameraState", "wideRoadCameraState"], ignore_alive_pubs=["wideRoadCameraState"], init_callback=get_car_params_callback, @@ -594,11 +553,10 @@ CONFIGS = [ pubs=["liveCalibration", "driverCameraState"], subs=["driverStateV2"], ignore=["logMonoTime", "driverStateV2.modelExecutionTime", "driverStateV2.gpuExecutionTime"], - should_recv_callback=dmonitoringmodeld_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("driverCameraState"), tolerance=NUMPY_TOLERANCE, processing_time=0.020, main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("driverCameraState").stream), - main_pub_drained=False, vision_pubs=["driverCameraState"], ignore_alive_pubs=["driverCameraState"], ), @@ -706,8 +664,8 @@ def _replay_multi_process( all_msgs = sorted(lr, key=lambda msg: msg.logMonoTime) log_msgs = [] + containers = [] try: - containers = [] for cfg in cfgs: container = ProcessContainer(cfg) containers.append(container) @@ -742,6 +700,11 @@ def _replay_multi_process( internal_pub_queue.append(m) heapq.heappush(internal_pub_index_heap, (m.logMonoTime, len(internal_pub_queue) - 1)) log_msgs.extend(output_msgs) + + # flush last set of messages from each process + for container in containers: + last_time = log_msgs[-1].logMonoTime if len(log_msgs) > 0 else int(time.monotonic() * 1e9) + log_msgs.extend(container.get_output_msgs(last_time)) finally: for container in containers: container.stop() diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 92669bec5..43f97e212 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -c289a0359d1b1f26cf4d9e73a2c04b2bbfec840f \ No newline at end of file +62b3e8590fe85f87494517b3177a0ccaad1e17e5 \ No newline at end of file diff --git a/selfdrive/test/setup_device_ci.sh b/selfdrive/test/setup_device_ci.sh index d71769851..98909bfb5 100755 --- a/selfdrive/test/setup_device_ci.sh +++ b/selfdrive/test/setup_device_ci.sh @@ -54,6 +54,7 @@ while true; do # /data/ciui.py & #fi + awk '{print \$1}' /proc/uptime > /var/tmp/power_watchdog sleep 5s done diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index cd702377e..935da99c1 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -55,6 +55,7 @@ PROCS = { "selfdrive.locationd.paramsd": 9.0, "selfdrive.locationd.lagd": 11.0, "selfdrive.ui.soundd": 3.0, + "selfdrive.ui.feedback.feedbackd": 1.0, "selfdrive.monitoring.dmonitoringd": 4.0, "./proclogd": 2.0, "system.logmessaged": 1.0, diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 5cff91b44..2a88832ef 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -70,14 +70,8 @@ if GetOption('extras'): qt_src.remove("main.cc") # replaced by test_runner qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs) - qt_env.SharedLibrary("qt/python_helpers", ["qt/qt_window.cc"], LIBS=qt_libs) - - # setup - qt_env.Program("qt/setup/setup", ["qt/setup/setup.cc", asset_obj], - LIBS=qt_libs + ['curl', 'common']) - + # build installers if arch != "Darwin": - # build installers raylib_env = env.Clone() raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/'] raylib_env['LINKFLAGS'].append('-Wl,-strip-debug') @@ -109,7 +103,3 @@ if GetOption('extras'): f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs) # keep installers small assert f[0].get_size() < 1900*1e3, f[0].get_size() - -# build watch3 -if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'): - qt_env.Program("watch3", ["watch3.cc"], LIBS=qt_libs + ['common', 'msgq', 'visionipc']) diff --git a/selfdrive/ui/feedback/feedbackd.py b/selfdrive/ui/feedback/feedbackd.py new file mode 100755 index 000000000..b02e5d97a --- /dev/null +++ b/selfdrive/ui/feedback/feedbackd.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +import cereal.messaging as messaging +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog +from cereal import car +from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER + +FEEDBACK_MAX_DURATION = 10.0 +ButtonType = car.CarState.ButtonEvent.Type + + +def main(): + params = Params() + pm = messaging.PubMaster(['userBookmark', 'audioFeedback']) + sm = messaging.SubMaster(['rawAudioData', 'bookmarkButton', 'carState']) + should_record_audio = False + block_num = 0 + waiting_for_release = False + early_stop_triggered = False + + while True: + sm.update() + should_send_bookmark = False + + if sm.updated['carState'] and sm['carState'].canValid: + for be in sm['carState'].buttonEvents: + if be.type == ButtonType.lkas: + if be.pressed: + if not should_record_audio: + if params.get_bool("RecordAudioFeedback"): # Start recording on first press if toggle set + should_record_audio = True + block_num = 0 + waiting_for_release = False + early_stop_triggered = False + cloudlog.info("LKAS button pressed - starting 10-second audio feedback") + else: + should_send_bookmark = True # immediately send bookmark if toggle false + cloudlog.info("LKAS button pressed - bookmarking") + elif should_record_audio and not waiting_for_release: # Wait for release of second press to stop recording early + waiting_for_release = True + elif waiting_for_release: # Second press released + waiting_for_release = False + early_stop_triggered = True + cloudlog.info("LKAS button released - ending recording early") + + if should_record_audio and sm.updated['rawAudioData']: + raw_audio = sm['rawAudioData'] + msg = messaging.new_message('audioFeedback', valid=True) + msg.audioFeedback.audio.data = raw_audio.data + msg.audioFeedback.audio.sampleRate = raw_audio.sampleRate + msg.audioFeedback.blockNum = block_num + block_num += 1 + if (block_num * SAMPLE_BUFFER / SAMPLE_RATE) >= FEEDBACK_MAX_DURATION or early_stop_triggered: # Check for timeout or early stop + should_send_bookmark = True # send bookmark at end of audio segment + should_record_audio = False + early_stop_triggered = False + cloudlog.info("10-second recording completed or second button press - stopping audio feedback") + pm.send('audioFeedback', msg) + + if sm.updated['bookmarkButton']: + cloudlog.info("Bookmark button pressed!") + should_send_bookmark = True + + if should_send_bookmark: + msg = messaging.new_message('userBookmark', valid=True) + pm.send('userBookmark', msg) + + +if __name__ == '__main__': + main() diff --git a/selfdrive/ui/layouts/main.py b/selfdrive/ui/layouts/main.py index 144d34e02..3ab8525d3 100644 --- a/selfdrive/ui/layouts/main.py +++ b/selfdrive/ui/layouts/main.py @@ -19,7 +19,7 @@ class MainLayout(Widget): def __init__(self): super().__init__() - self._pm = messaging.PubMaster(['userFlag']) + self._pm = messaging.PubMaster(['bookmarkButton']) self._sidebar = Sidebar() self._current_mode = MainState.HOME @@ -40,7 +40,7 @@ class MainLayout(Widget): def _setup_callbacks(self): self._sidebar.set_callbacks(on_settings=self._on_settings_clicked, - on_flag=self._on_flag_clicked) + on_flag=self._on_bookmark_clicked) self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE)) self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state) self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked) @@ -76,10 +76,10 @@ class MainLayout(Widget): def _on_settings_clicked(self): self.open_settings(PanelType.DEVICE) - def _on_flag_clicked(self): - user_flag = messaging.new_message('userFlag') - user_flag.valid = True - self._pm.send('userFlag', user_flag) + def _on_bookmark_clicked(self): + user_bookmark = messaging.new_message('bookmarkButton') + user_bookmark.valid = True + self._pm.send('bookmarkButton', user_bookmark) def _on_onroad_clicked(self): self._sidebar.set_visible(not self._sidebar.is_visible) diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py index 58afcec5e..ff0564a61 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -23,6 +23,10 @@ DESCRIPTIONS = { 'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.", "IsMetric": "Display speed in km/h instead of mph.", "RecordAudio": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.", + "RecordAudioFeedback": ( + "Press the LKAS button to record audio feedback about openpilot. When this toggle is disabled, the button acts as a bookmark button. " + + "The event will be highlighted in comma connect and the segment will be preserved on your device's storage." + ), } @@ -81,6 +85,12 @@ class TogglesLayout(Widget): self._params.get_bool("RecordAudio"), icon="microphone.png", ), + toggle_item( + "Record Audio Feedback with LKAS button", + DESCRIPTIONS["RecordAudioFeedback"], + self._params.get_bool("RecordAudioFeedback"), + icon="microphone.png", + ), toggle_item( "Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="metric.png" ), diff --git a/selfdrive/ui/layouts/sidebar.py b/selfdrive/ui/layouts/sidebar.py index 82d499716..6af947a10 100644 --- a/selfdrive/ui/layouts/sidebar.py +++ b/selfdrive/ui/layouts/sidebar.py @@ -146,7 +146,7 @@ class Sidebar(Widget): def _draw_buttons(self, rect: rl.Rectangle): mouse_pos = rl.get_mouse_position() - mouse_down = self._is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) + mouse_down = self.is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) # Settings button settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN) diff --git a/selfdrive/ui/onroad/cameraview.py b/selfdrive/ui/onroad/cameraview.py index cc2c6b4fe..ca031e2f1 100644 --- a/selfdrive/ui/onroad/cameraview.py +++ b/selfdrive/ui/onroad/cameraview.py @@ -141,6 +141,9 @@ class CameraView(Widget): self.client = None + def __del__(self): + self.close() + def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: if not self.frame: return np.eye(3) @@ -337,16 +340,7 @@ class CameraView(Widget): if __name__ == "__main__": - gui_app.init_window("watch3") - road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) - driver_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) - wide_road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD) - try: - for _ in gui_app.render(): - road_camera_view.render(rl.Rectangle(gui_app.width // 4, 0, gui_app.width // 2, gui_app.height // 2)) - driver_camera_view.render(rl.Rectangle(0, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) - wide_road_camera_view.render(rl.Rectangle(gui_app.width // 2, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) - finally: - road_camera_view.close() - driver_camera_view.close() - wide_road_camera_view.close() + gui_app.init_window("camera view") + road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) + for _ in gui_app.render(): + road.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) diff --git a/selfdrive/ui/onroad/exp_button.py b/selfdrive/ui/onroad/exp_button.py index e6748e4e0..27f576307 100644 --- a/selfdrive/ui/onroad/exp_button.py +++ b/selfdrive/ui/onroad/exp_button.py @@ -50,7 +50,7 @@ class ExpButton(Widget): center_y = int(self._rect.y + self._rect.height // 2) mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect) - mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed + mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255 texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel diff --git a/selfdrive/ui/onroad/hud_renderer.py b/selfdrive/ui/onroad/hud_renderer.py index a74d309e0..4ae713d9d 100644 --- a/selfdrive/ui/onroad/hud_renderer.py +++ b/selfdrive/ui/onroad/hud_renderer.py @@ -1,6 +1,6 @@ import pyray as rl from dataclasses import dataclass -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.selfdrive.ui.onroad.exp_button import ExpButton from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app, FontWeight diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 205a14624..f8a46257f 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -75,6 +75,13 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { "../assets/icons/microphone.png", true, }, + { + "RecordAudioFeedback", + tr("Record Audio Feedback with LKAS button"), + tr("Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.\n\nNote that this feature is only compatible with select cars."), + "../assets/icons/microphone.png", + false, + }, { "IsMetric", tr("Use Metric System"), diff --git a/selfdrive/ui/qt/python_helpers.py b/selfdrive/ui/qt/python_helpers.py deleted file mode 100644 index 1f6d43f30..000000000 --- a/selfdrive/ui/qt/python_helpers.py +++ /dev/null @@ -1,23 +0,0 @@ -import os -import platform -from cffi import FFI - -import sip - -from openpilot.common.basedir import BASEDIR - -def suffix(): - return ".dylib" if platform.system() == "Darwin" else ".so" - - -def get_ffi(): - lib = os.path.join(BASEDIR, "selfdrive", "ui", "qt", "libpython_helpers" + suffix()) - - ffi = FFI() - ffi.cdef("void set_main_window(void *w);") - return ffi, ffi.dlopen(lib) - - -def set_main_window(widget): - ffi, lib = get_ffi() - lib.set_main_window(ffi.cast('void*', sip.unwrapinstance(widget))) diff --git a/selfdrive/ui/qt/setup/setup.cc b/selfdrive/ui/qt/setup/setup.cc deleted file mode 100644 index 90ed973b0..000000000 --- a/selfdrive/ui/qt/setup/setup.cc +++ /dev/null @@ -1,541 +0,0 @@ -#include "selfdrive/ui/qt/setup/setup.h" - -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include "common/util.h" -#include "system/hardware/hw.h" -#include "selfdrive/ui/qt/api.h" -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/network/networking.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/input.h" - -const std::string USER_AGENT = "AGNOSSetup-"; -const QString OPENPILOT_URL = "https://openpilot.comma.ai"; - -bool is_elf(char *fname) { - FILE *fp = fopen(fname, "rb"); - if (fp == NULL) { - return false; - } - char buf[4]; - size_t n = fread(buf, 1, 4, fp); - fclose(fp); - return n == 4 && buf[0] == 0x7f && buf[1] == 'E' && buf[2] == 'L' && buf[3] == 'F'; -} - -void Setup::download(QString url) { - // autocomplete incomplete urls - if (QRegularExpression("^([^/.]+)/([^/]+)$").match(url).hasMatch()) { - url.prepend("https://installer.comma.ai/"); - } - - CURL *curl = curl_easy_init(); - if (!curl) { - emit finished(url, tr("Something went wrong. Reboot the device.")); - return; - } - - auto version = util::read_file("/VERSION"); - - struct curl_slist *list = NULL; - std::string header = "X-openpilot-serial: " + Hardware::get_serial(); - list = curl_slist_append(list, header.c_str()); - - char tmpfile[] = "/tmp/installer_XXXXXX"; - FILE *fp = fdopen(mkstemp(tmpfile), "wb"); - - curl_easy_setopt(curl, CURLOPT_URL, url.toStdString().c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); - curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_USERAGENT, (USER_AGENT + version).c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); - - int ret = curl_easy_perform(curl); - long res_status = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &res_status); - - if (ret != CURLE_OK || res_status != 200) { - emit finished(url, tr("Ensure the entered URL is valid, and the device’s internet connection is good.")); - } else if (!is_elf(tmpfile)) { - emit finished(url, tr("No custom software found at this URL.")); - } else { - rename(tmpfile, "/tmp/installer"); - - FILE *fp_url = fopen("/tmp/installer_url", "w"); - fprintf(fp_url, "%s", url.toStdString().c_str()); - fclose(fp_url); - - emit finished(url); - } - - curl_slist_free_all(list); - curl_easy_cleanup(curl); - fclose(fp); -} - -QWidget * Setup::low_voltage() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 0, 55, 55); - main_layout->setSpacing(0); - - // inner text layout: warning icon, title, and body - QVBoxLayout *inner_layout = new QVBoxLayout(); - inner_layout->setContentsMargins(110, 144, 365, 0); - main_layout->addLayout(inner_layout); - - QLabel *triangle = new QLabel(); - triangle->setPixmap(QPixmap(ASSET_PATH + "icons/warning.png")); - inner_layout->addWidget(triangle, 0, Qt::AlignTop | Qt::AlignLeft); - inner_layout->addSpacing(80); - - QLabel *title = new QLabel(tr("WARNING: Low Voltage")); - title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;"); - inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - inner_layout->addSpacing(25); - - QLabel *body = new QLabel(tr("Power your device in a car with a harness or proceed at your own risk.")); - body->setWordWrap(true); - body->setAlignment(Qt::AlignTop | Qt::AlignLeft); - body->setStyleSheet("font-size: 80px; font-weight: 300;"); - inner_layout->addWidget(body); - - inner_layout->addStretch(); - - // power off + continue buttons - QHBoxLayout *blayout = new QHBoxLayout(); - blayout->setSpacing(50); - main_layout->addLayout(blayout, 0); - - QPushButton *poweroff = new QPushButton(tr("Power off")); - poweroff->setObjectName("navBtn"); - blayout->addWidget(poweroff); - QObject::connect(poweroff, &QPushButton::clicked, this, [=]() { - Hardware::poweroff(); - }); - - QPushButton *cont = new QPushButton(tr("Continue")); - cont->setObjectName("navBtn"); - blayout->addWidget(cont); - QObject::connect(cont, &QPushButton::clicked, this, &Setup::nextPage); - - return widget; -} - -QWidget * Setup::custom_software_warning() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 0, 55, 55); - main_layout->setSpacing(0); - - QVBoxLayout *inner_layout = new QVBoxLayout(); - inner_layout->setContentsMargins(110, 110, 300, 0); - main_layout->addLayout(inner_layout); - - QLabel *title = new QLabel(tr("WARNING: Custom Software")); - title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;"); - inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - inner_layout->addSpacing(25); - - QLabel *body = new QLabel(tr("Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.\n\nIf you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later.")); - body->setWordWrap(true); - body->setAlignment(Qt::AlignTop | Qt::AlignLeft); - body->setStyleSheet("font-size: 65px; font-weight: 300;"); - inner_layout->addWidget(body); - - inner_layout->addStretch(); - - QHBoxLayout *blayout = new QHBoxLayout(); - blayout->setSpacing(50); - main_layout->addLayout(blayout, 0); - - QPushButton *back = new QPushButton(tr("Back")); - back->setObjectName("navBtn"); - blayout->addWidget(back); - QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); - - QPushButton *cont = new QPushButton(tr("Continue")); - cont->setObjectName("navBtn"); - blayout->addWidget(cont); - QObject::connect(cont, &QPushButton::clicked, this, [=]() { - QTimer::singleShot(0, [=]() { - setCurrentWidget(downloading_widget); - }); - QString url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software")); - if (!url.isEmpty()) { - QTimer::singleShot(1000, this, [=]() { - download(url); - }); - } else { - setCurrentWidget(software_selection_widget); - } - }); - - return widget; -} - -QWidget * Setup::getting_started() { - QWidget *widget = new QWidget(); - - QHBoxLayout *main_layout = new QHBoxLayout(widget); - main_layout->setMargin(0); - - QVBoxLayout *vlayout = new QVBoxLayout(); - vlayout->setContentsMargins(165, 280, 100, 0); - main_layout->addLayout(vlayout); - - QLabel *title = new QLabel(tr("Getting Started")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - vlayout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - vlayout->addSpacing(90); - QLabel *desc = new QLabel(tr("Before we get on the road, let’s finish installation and cover some details.")); - desc->setWordWrap(true); - desc->setStyleSheet("font-size: 80px; font-weight: 300;"); - vlayout->addWidget(desc, 0, Qt::AlignTop | Qt::AlignLeft); - - vlayout->addStretch(); - - QPushButton *btn = new QPushButton(); - btn->setIcon(QIcon(":/images/button_continue_triangle.svg")); - btn->setIconSize(QSize(54, 106)); - btn->setFixedSize(310, 1080); - btn->setProperty("primary", true); - btn->setStyleSheet("border: none;"); - main_layout->addWidget(btn, 0, Qt::AlignRight); - QObject::connect(btn, &QPushButton::clicked, this, &Setup::nextPage); - - return widget; -} - -QWidget * Setup::network_setup() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 50, 55, 50); - - // title - QLabel *title = new QLabel(tr("Connect to Wi-Fi")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); - - main_layout->addSpacing(25); - - // wifi widget - Networking *networking = new Networking(this, false); - networking->setStyleSheet("Networking {background-color: #292929; border-radius: 13px;}"); - main_layout->addWidget(networking, 1); - - main_layout->addSpacing(35); - - // back + continue buttons - QHBoxLayout *blayout = new QHBoxLayout; - main_layout->addLayout(blayout); - blayout->setSpacing(50); - - QPushButton *back = new QPushButton(tr("Back")); - back->setObjectName("navBtn"); - QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); - blayout->addWidget(back); - - QPushButton *cont = new QPushButton(); - cont->setObjectName("navBtn"); - cont->setProperty("primary", true); - cont->setEnabled(false); - QObject::connect(cont, &QPushButton::clicked, this, &Setup::nextPage); - blayout->addWidget(cont); - - // setup timer for testing internet connection - HttpRequest *request = new HttpRequest(this, false, 2500); - QObject::connect(request, &HttpRequest::requestDone, [=](const QString &, bool success) { - cont->setEnabled(success); - if (success) { - const bool wifi = networking->wifi->currentNetworkType() == NetworkType::WIFI; - cont->setText(wifi ? tr("Continue") : tr("Continue without Wi-Fi")); - } else { - cont->setText(tr("Waiting for internet")); - } - repaint(); - }); - request->sendRequest(OPENPILOT_URL); - QTimer *timer = new QTimer(this); - QObject::connect(timer, &QTimer::timeout, [=]() { - if (!request->active() && cont->isVisible()) { - request->sendRequest(OPENPILOT_URL); - } - }); - timer->start(1000); - - return widget; -} - -QWidget * radio_button(QString title, QButtonGroup *group) { - QPushButton *btn = new QPushButton(title); - btn->setCheckable(true); - group->addButton(btn); - btn->setStyleSheet(R"( - QPushButton { - height: 230; - padding-left: 100px; - padding-right: 100px; - text-align: left; - font-size: 80px; - font-weight: 400; - border-radius: 10px; - background-color: #4F4F4F; - } - QPushButton:checked { - background-color: #465BEA; - } - )"); - - // checkmark icon - QPixmap pix(":/icons/circled_check.svg"); - btn->setIcon(pix); - btn->setIconSize(QSize(0, 0)); - btn->setLayoutDirection(Qt::RightToLeft); - QObject::connect(btn, &QPushButton::toggled, [=](bool checked) { - btn->setIconSize(checked ? QSize(104, 104) : QSize(0, 0)); - }); - return btn; -} - -QWidget * Setup::software_selection() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 50, 55, 50); - main_layout->setSpacing(0); - - // title - QLabel *title = new QLabel(tr("Choose Software to Install")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); - - main_layout->addSpacing(50); - - // sunnypilot + custom radio buttons - QButtonGroup *group = new QButtonGroup(widget); - group->setExclusive(true); - - QWidget *openpilot = radio_button(tr("sunnypilot"), group); - main_layout->addWidget(openpilot); - - main_layout->addSpacing(30); - - QWidget *custom = radio_button(tr("Custom Software"), group); - main_layout->addWidget(custom); - - main_layout->addStretch(); - - // back + continue buttons - QHBoxLayout *blayout = new QHBoxLayout; - main_layout->addLayout(blayout); - blayout->setSpacing(50); - - QPushButton *back = new QPushButton(tr("Back")); - back->setObjectName("navBtn"); - QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); - blayout->addWidget(back); - - QPushButton *cont = new QPushButton(tr("Continue")); - cont->setObjectName("navBtn"); - cont->setEnabled(false); - cont->setProperty("primary", true); - blayout->addWidget(cont); - - QObject::connect(cont, &QPushButton::clicked, [=]() { - if (group->checkedButton() != openpilot) { - QTimer::singleShot(0, [=]() { - setCurrentWidget(custom_software_warning_widget); - }); - } else { - QTimer::singleShot(0, [=]() { - setCurrentWidget(downloading_widget); - }); - QTimer::singleShot(1000, this, [=]() { - download(OPENPILOT_URL); - }); - } - }); - - connect(group, QOverload::of(&QButtonGroup::buttonClicked), [=](QAbstractButton *btn) { - btn->setChecked(true); - cont->setEnabled(true); - }); - - return widget; -} - -QWidget * Setup::downloading() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - QLabel *txt = new QLabel(tr("Downloading...")); - txt->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(txt, 0, Qt::AlignCenter); - return widget; -} - -QWidget * Setup::download_failed(QLabel *url, QLabel *body) { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 185, 55, 55); - main_layout->setSpacing(0); - - QLabel *title = new QLabel(tr("Download Failed")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - main_layout->addSpacing(67); - - url->setWordWrap(true); - url->setAlignment(Qt::AlignTop | Qt::AlignLeft); - url->setStyleSheet("font-family: \"JetBrains Mono\"; font-size: 64px; font-weight: 400; margin-right: 100px;"); - main_layout->addWidget(url); - - main_layout->addSpacing(48); - - body->setWordWrap(true); - body->setAlignment(Qt::AlignTop | Qt::AlignLeft); - body->setStyleSheet("font-size: 80px; font-weight: 300; margin-right: 100px;"); - main_layout->addWidget(body); - - main_layout->addStretch(); - - // reboot + start over buttons - QHBoxLayout *blayout = new QHBoxLayout(); - blayout->setSpacing(50); - main_layout->addLayout(blayout, 0); - - QPushButton *reboot = new QPushButton(tr("Reboot device")); - reboot->setObjectName("navBtn"); - blayout->addWidget(reboot); - QObject::connect(reboot, &QPushButton::clicked, this, [=]() { - Hardware::reboot(); - }); - - QPushButton *restart = new QPushButton(tr("Start over")); - restart->setObjectName("navBtn"); - restart->setProperty("primary", true); - blayout->addWidget(restart); - QObject::connect(restart, &QPushButton::clicked, this, [=]() { - setCurrentIndex(1); - }); - - widget->setStyleSheet(R"( - QLabel { - margin-left: 117; - } - )"); - return widget; -} - -void Setup::prevPage() { - setCurrentIndex(currentIndex() - 1); -} - -void Setup::nextPage() { - setCurrentIndex(currentIndex() + 1); -} - -Setup::Setup(QWidget *parent) : QStackedWidget(parent) { - if (std::getenv("MULTILANG")) { - selectLanguage(); - } - - std::stringstream buffer; - buffer << std::ifstream("/sys/class/hwmon/hwmon1/in1_input").rdbuf(); - float voltage = (float)std::atoi(buffer.str().c_str()) / 1000.; - if (voltage < 7) { - addWidget(low_voltage()); - } - - addWidget(getting_started()); - addWidget(network_setup()); - software_selection_widget = software_selection(); - addWidget(software_selection_widget); - custom_software_warning_widget = custom_software_warning(); - addWidget(custom_software_warning_widget); - downloading_widget = downloading(); - addWidget(downloading_widget); - - QLabel *url_label = new QLabel(); - QLabel *body_label = new QLabel(); - failed_widget = download_failed(url_label, body_label); - addWidget(failed_widget); - - QObject::connect(this, &Setup::finished, [=](const QString &url, const QString &error) { - qDebug() << "finished" << url << error; - if (error.isEmpty()) { - // hide setup on success - QTimer::singleShot(3000, this, &QWidget::hide); - } else { - url_label->setText(url); - body_label->setText(error); - setCurrentWidget(failed_widget); - } - }); - - // TODO: revisit pressed bg color - setStyleSheet(R"( - * { - color: white; - font-family: Inter; - } - Setup { - background-color: black; - } - QPushButton#navBtn { - height: 160; - font-size: 55px; - font-weight: 400; - border-radius: 10px; - background-color: #333333; - } - QPushButton#navBtn:disabled, QPushButton[primary='true']:disabled { - color: #808080; - background-color: #333333; - } - QPushButton#navBtn:pressed { - background-color: #444444; - } - QPushButton[primary='true'], #navBtn[primary='true'] { - background-color: #465BEA; - } - QPushButton[primary='true']:pressed, #navBtn:pressed[primary='true'] { - background-color: #3049F4; - } - )"); -} - -void Setup::selectLanguage() { - QMap langs = getSupportedLanguages(); - QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), "", this); - if (!selection.isEmpty()) { - QString selectedLang = langs[selection]; - Params().put("LanguageSetting", selectedLang.toStdString()); - if (translator.load(":/" + selectedLang)) { - qApp->installTranslator(&translator); - } - } -} - -int main(int argc, char *argv[]) { - QApplication a(argc, argv); - Setup setup; - setMainWindow(&setup); - return a.exec(); -} diff --git a/selfdrive/ui/qt/setup/setup.h b/selfdrive/ui/qt/setup/setup.h deleted file mode 100644 index 986956c90..000000000 --- a/selfdrive/ui/qt/setup/setup.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -class Setup : public QStackedWidget { - Q_OBJECT - -public: - explicit Setup(QWidget *parent = 0); - -private: - void selectLanguage(); - QWidget *low_voltage(); - QWidget *custom_software_warning(); - QWidget *getting_started(); - QWidget *network_setup(); - QWidget *software_selection(); - QWidget *downloading(); - QWidget *download_failed(QLabel *url, QLabel *body); - - QWidget *failed_widget; - QWidget *downloading_widget; - QWidget *custom_software_warning_widget; - QWidget *software_selection_widget; - QTranslator translator; - -signals: - void finished(const QString &url, const QString &error = ""); - -public slots: - void nextPage(); - void prevPage(); - void download(QString url); -}; diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc index c63cc0159..d7e5e6c9d 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -38,7 +38,7 @@ Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed( QObject::connect(uiState(), &UIState::uiUpdate, this, &Sidebar::updateState); - pm = std::make_unique(std::vector{"userFlag"}); + pm = std::make_unique(std::vector{"bookmarkButton"}); } void Sidebar::mousePressEvent(QMouseEvent *event) { @@ -61,8 +61,8 @@ void Sidebar::mouseReleaseEvent(QMouseEvent *event) { } if (onroad && home_btn.contains(event->pos())) { MessageBuilder msg; - msg.initEvent().initUserFlag(); - pm->send("userFlag", msg); + msg.initEvent().initBookmarkButton(); + pm->send("bookmarkButton", msg); } else if (settings_btn.contains(event->pos())) { emit openSettings(); } else if (recording_audio && mic_indicator_btn.contains(event->pos())) { diff --git a/selfdrive/ui/tests/test_feedbackd.py b/selfdrive/ui/tests/test_feedbackd.py new file mode 100644 index 000000000..c2d81aef8 --- /dev/null +++ b/selfdrive/ui/tests/test_feedbackd.py @@ -0,0 +1,52 @@ +import pytest +import cereal.messaging as messaging +from cereal import car +from openpilot.common.params import Params +from openpilot.system.manager.process_config import managed_processes + + +class TestFeedbackd: + def setup_method(self): + self.pm = messaging.PubMaster(['carState', 'rawAudioData']) + self.sm = messaging.SubMaster(['audioFeedback']) + + def _send_lkas_button(self, pressed: bool): + msg = messaging.new_message('carState') + msg.carState.canValid = True + msg.carState.buttonEvents = [{'type': car.CarState.ButtonEvent.Type.lkas, 'pressed': pressed}] + self.pm.send('carState', msg) + + def _send_audio_data(self, count: int = 5): + for _ in range(count): + audio_msg = messaging.new_message('rawAudioData') + audio_msg.rawAudioData.data = bytes(1600) # 800 samples of int16 + audio_msg.rawAudioData.sampleRate = 16000 + self.pm.send('rawAudioData', audio_msg) + self.sm.update(timeout=100) + + @pytest.mark.parametrize("record_feedback", [False, True]) + def test_audio_feedback(self, record_feedback): + Params().put_bool("RecordAudioFeedback", record_feedback) + + managed_processes["feedbackd"].start() + assert self.pm.wait_for_readers_to_update('carState', timeout=5) + assert self.pm.wait_for_readers_to_update('rawAudioData', timeout=5) + + self._send_lkas_button(pressed=True) + self._send_audio_data() + self._send_lkas_button(pressed=False) + self._send_audio_data() + + if record_feedback: + assert self.sm.updated['audioFeedback'], "audioFeedback should be published when enabled" + else: + assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published when disabled" + + self._send_lkas_button(pressed=True) + self._send_audio_data() + self._send_lkas_button(pressed=False) + self._send_audio_data() + + assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published after second press" + + managed_processes["feedbackd"].stop() diff --git a/selfdrive/ui/tests/test_raylib_ui.py b/selfdrive/ui/tests/test_raylib_ui.py new file mode 100644 index 000000000..3f2330197 --- /dev/null +++ b/selfdrive/ui/tests/test_raylib_ui.py @@ -0,0 +1,8 @@ +import time +from openpilot.selfdrive.test.helpers import with_processes + + +@with_processes(["raylib_ui"]) +def test_raylib_ui(): + """Test initialization of the UI widgets is successful.""" + time.sleep(1) diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 7d3d9edd0..eac3b561f 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -507,7 +507,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp تأخير التحديث - openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. @@ -1157,6 +1157,16 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 3489b4860..b91c0e23c 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -499,7 +499,7 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere Update pausieren - openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. @@ -1139,6 +1139,16 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 5807accb9..79b10f316 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -503,7 +503,7 @@ El Modo Firehose te permite maximizar las subidas de datos de entrenamiento para Posponer Actualización - openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. @@ -1143,6 +1143,16 @@ Si desea continuar, utilice https://flash.comma.ai para restaurar su dispositivo Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. Graba y almacena el audio del micrófono mientras conduces. El audio se incluirá en el video de la cámara del tablero en comma connect. + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index ba1eb93b3..a4effb2a4 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -497,7 +497,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Reporter la mise à jour - openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. @@ -1135,6 +1135,16 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index fea6e51c0..8d0b2cb75 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -501,8 +501,8 @@ Firehoseモードを有効にすると学習データを最大限アップロー また後で更新する - openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. - openpilotが過剰な%1の作動を検出しました。ソフトウェアの不具合の可能性があります。https://comma.ai/support からサポートへご連絡下さい。 + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + @@ -1138,6 +1138,16 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. 運転中にマイク音声を録音・保存します。音声は comma connect のドライブレコーダー映像に含まれます。 + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index aaefdb92b..3cffab10b 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -501,8 +501,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 업데이트 일시 중지 - openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. - 오픈파일럿은 과도한 %1 작동을 감지했습니다. 소프트웨어 버그 때문일 수 있습니다. https://comma.ai/support 에 문의하여 지원받으세요. + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + @@ -1138,6 +1138,16 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. 운전 중에 마이크 오디오를 녹음하고 저장하십시오. 오디오는 comma connect의 대시캠 비디오에 포함됩니다. + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 976fc711d..e16872c75 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -503,8 +503,8 @@ O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar Adiar Atualização - openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. - O openpilot detectou uma atuação excessiva de %1. Isso pode ser causado por uma falha no software. Por favor, entre em contato com o suporte em https://comma.ai/support. + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. + @@ -1143,6 +1143,16 @@ Se quiser continuar, use https://flash.comma.ai para restaurar seu dispositivo a Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. Grave e armazene o áudio do microfone enquanto estiver dirigindo. O áudio será incluído ao vídeo dashcam no comma connect. + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 3821b3b80..87cb8bc2e 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -497,7 +497,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp เลื่อนการอัปเดต - openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. @@ -1132,6 +1132,16 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 6cafb1dff..6112698e3 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -494,7 +494,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Güncellemeyi sessize al - openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. @@ -1129,6 +1129,16 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 5e5b9da31..a1d160237 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -501,7 +501,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 暂停更新 - openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. @@ -1138,6 +1138,16 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. 在驾驶时录制并存储麦克风音频。该音频将会包含在 comma connect 的行车记录仪视频中。 + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 630dbf2a0..a094dd418 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -501,7 +501,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 暫停更新 - openpilot has detected excessive %1 actuation. This may be due to a software bug. Please contact support at https://comma.ai/support. + openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting. @@ -1138,6 +1138,16 @@ If you'd like to proceed, use https://flash.comma.ai to restore your device Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. 在駕駛時錄製並儲存麥克風音訊。此音訊將會收錄在 comma connect 的行車記錄器影片中。 + + Record Audio Feedback with LKAS button + + + + Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. + +Note that this feature is only compatible with select cars. + + WiFiPromptWidget diff --git a/selfdrive/ui/watch3.cc b/selfdrive/ui/watch3.cc deleted file mode 100644 index 258e2a7bd..000000000 --- a/selfdrive/ui/watch3.cc +++ /dev/null @@ -1,33 +0,0 @@ -#include -#include - -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/cameraview.h" - -int main(int argc, char *argv[]) { - initApp(argc, argv); - - QApplication a(argc, argv); - QWidget w; - setMainWindow(&w); - - QVBoxLayout *layout = new QVBoxLayout(&w); - layout->setMargin(0); - layout->setSpacing(0); - - { - QHBoxLayout *hlayout = new QHBoxLayout(); - layout->addLayout(hlayout); - hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_ROAD)); - } - - { - QHBoxLayout *hlayout = new QHBoxLayout(); - layout->addLayout(hlayout); - hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_DRIVER)); - hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_WIDE_ROAD)); - } - - return a.exec(); -} diff --git a/selfdrive/ui/watch3.py b/selfdrive/ui/watch3.py new file mode 100755 index 000000000..bb64cdc4d --- /dev/null +++ b/selfdrive/ui/watch3.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +import pyray as rl + +from msgq.visionipc import VisionStreamType +from openpilot.system.ui.lib.application import gui_app +from openpilot.selfdrive.ui.onroad.cameraview import CameraView + + +if __name__ == "__main__": + gui_app.init_window("watch3") + road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) + driver = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) + wide = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD) + for _ in gui_app.render(): + road.render(rl.Rectangle(gui_app.width // 4, 0, gui_app.width // 2, gui_app.height // 2)) + driver.render(rl.Rectangle(0, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) + wide.render(rl.Rectangle(gui_app.width // 2, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) diff --git a/selfdrive/ui/widgets/exp_mode_button.py b/selfdrive/ui/widgets/exp_mode_button.py index be50c59d0..88664e0fa 100644 --- a/selfdrive/ui/widgets/exp_mode_button.py +++ b/selfdrive/ui/widgets/exp_mode_button.py @@ -14,7 +14,6 @@ class ExperimentalModeButton(Widget): self.params = Params() self.experimental_mode = self.params.get_bool("ExperimentalMode") - self.is_pressed = False self.chill_pixmap = gui_app.texture("icons/couch.png", self.img_width, self.img_width) self.experimental_pixmap = gui_app.texture("icons/experimental_grey.png", self.img_width, self.img_width) @@ -32,19 +31,12 @@ class ExperimentalModeButton(Widget): rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height), start_color, end_color) - def _handle_interaction(self, rect): - mouse_pos = rl.get_mouse_position() - mouse_in_rect = rl.check_collision_point_rec(mouse_pos, rect) - - self.is_pressed = mouse_in_rect and rl.is_mouse_button_down(rl.MOUSE_BUTTON_LEFT) - return mouse_in_rect and rl.is_mouse_button_released(rl.MOUSE_BUTTON_LEFT) + def _handle_mouse_release(self, mouse_pos): + self.experimental_mode = not self.experimental_mode + # TODO: Opening settings for ExperimentalMode + self.params.put_bool("ExperimentalMode", self.experimental_mode) def _render(self, rect): - if self._handle_interaction(rect): - self.experimental_mode = not self.experimental_mode - # TODO: Opening settings for ExperimentalMode - self.params.put_bool("ExperimentalMode", self.experimental_mode) - rl.draw_rectangle_rounded(rect, 0.08, 20, rl.Color(255, 255, 255, 255)) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) diff --git a/sunnypilot/navd/helpers.py b/sunnypilot/navd/helpers.py index 85894fb2a..c57706d32 100644 --- a/sunnypilot/navd/helpers.py +++ b/sunnypilot/navd/helpers.py @@ -5,7 +5,7 @@ import math import numpy as np from typing import Any, cast -from openpilot.common.conversions import Conversions +from openpilot.common.constants import CV from openpilot.common.params import Params DIRECTIONS = ('left', 'right', 'straight') @@ -13,8 +13,8 @@ MODIFIABLE_DIRECTIONS = ('left', 'right') EARTH_MEAN_RADIUS = 6371007.2 SPEED_CONVERSIONS = { - 'km/h': Conversions.KPH_TO_MS, - 'mph': Conversions.MPH_TO_MS, + 'km/h': CV.KPH_TO_MS, + 'mph': CV.MPH_TO_MS, } @@ -133,7 +133,7 @@ def string_to_direction(direction: str) -> str: def maxspeed_to_ms(maxspeed: dict[str, str | float]) -> float: unit = cast(str, maxspeed['unit']) speed = cast(float, maxspeed['speed']) - return SPEED_CONVERSIONS[unit] * speed + return float(SPEED_CONVERSIONS[unit] * speed) def field_valid(dat: dict, field: str) -> bool: diff --git a/sunnypilot/selfdrive/car/tests/test_custom_cruise.py b/sunnypilot/selfdrive/car/tests/test_custom_cruise.py index 4b7f9b819..3bed8534e 100644 --- a/sunnypilot/selfdrive/car/tests/test_custom_cruise.py +++ b/sunnypilot/selfdrive/car/tests/test_custom_cruise.py @@ -2,7 +2,7 @@ import pytest from parameterized import parameterized_class from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.selfdrive.car.cruise import V_CRUISE_INITIAL from openpilot.selfdrive.car.tests.test_cruise_speed import TestVCruiseHelper diff --git a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py index 6f8d99b75..2f27e7e42 100644 --- a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py @@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index 466bae7e6..77d46ace3 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -8,7 +8,7 @@ from collections import deque import math import numpy as np -from opendbc.car import FRICTION_THRESHOLD, get_friction +from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.selfdrive.modeld.constants import ModelConstants diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py index 6ba1dcef4..ca8f16fe7 100644 --- a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py @@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral diff --git a/system/hardware/base.h b/system/hardware/base.h index baf0f3c3d..df9700a01 100644 --- a/system/hardware/base.h +++ b/system/hardware/base.h @@ -10,9 +10,6 @@ // no-op base hw class class HardwareNone { public: - static constexpr float MAX_VOLUME = 0.7; - static constexpr float MIN_VOLUME = 0.2; - static std::string get_os_version() { return ""; } static std::string get_name() { return ""; } static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::UNKNOWN; } diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index ec2557422..7d99793a5 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -1,25 +1,25 @@ [ { "name": "xbl", - "url": "https://commadist.azureedge.net/agnosupdate/xbl-6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1.img.xz", - "hash": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", - "hash_raw": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", + "url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz", + "hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", + "hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", "size": 3282256, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "003a17ab1be68a696f7efe4c9938e8be511d4aacfc2f3211fc896bdc1681d089" + "ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89" }, { "name": "xbl_config", - "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535.img.xz", - "hash": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", - "hash_raw": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", + "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz", + "hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", + "hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", "size": 98124, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "2a855dd636cc94718b64bea83a44d0a31741ecaa8f72a63613ff348ec7404091" + "ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f" }, { "name": "abl", @@ -56,28 +56,28 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz", - "hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "size": 18479104, + "url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz", + "hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "size": 18515968, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a" + "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz", - "hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164", - "hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", + "url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img.xz", + "hash": "49faee0e9b084abf0ea46f87722e3366bbd0435fb6b25cce189295c1ff368da1", + "hash_raw": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643", "size": 5368709120, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec", + "ondevice_hash": "db07761be0130e35a9d3ea6bec8df231260d3e767ae770850f18f10e14d0ab3f", "alt": { - "hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img", + "hash": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643", + "url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img", "size": 5368709120 } } diff --git a/system/hardware/tici/all-partitions.json b/system/hardware/tici/all-partitions.json index d28b48128..864854499 100644 --- a/system/hardware/tici/all-partitions.json +++ b/system/hardware/tici/all-partitions.json @@ -97,14 +97,14 @@ }, { "name": "persist", - "url": "https://commadist.azureedge.net/agnosupdate/persist-9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d.img.xz", - "hash": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d", - "hash_raw": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d", - "size": 33554432, + "url": "https://commadist.azureedge.net/agnosupdate/persist-d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786.img.xz", + "hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786", + "hash_raw": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786", + "size": 4096, "sparse": false, "full_check": true, "has_ab": false, - "ondevice_hash": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d" + "ondevice_hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786" }, { "name": "systemrw", @@ -130,25 +130,25 @@ }, { "name": "xbl", - "url": "https://commadist.azureedge.net/agnosupdate/xbl-6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1.img.xz", - "hash": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", - "hash_raw": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", + "url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz", + "hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", + "hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", "size": 3282256, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "003a17ab1be68a696f7efe4c9938e8be511d4aacfc2f3211fc896bdc1681d089" + "ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89" }, { "name": "xbl_config", - "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535.img.xz", - "hash": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", - "hash_raw": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", + "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz", + "hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", + "hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", "size": 98124, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "2a855dd636cc94718b64bea83a44d0a31741ecaa8f72a63613ff348ec7404091" + "ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f" }, { "name": "abl", @@ -339,62 +339,62 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz", - "hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "size": 18479104, + "url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz", + "hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "size": 18515968, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a" + "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz", - "hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164", - "hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", + "url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img.xz", + "hash": "49faee0e9b084abf0ea46f87722e3366bbd0435fb6b25cce189295c1ff368da1", + "hash_raw": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643", "size": 5368709120, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec", + "ondevice_hash": "db07761be0130e35a9d3ea6bec8df231260d3e767ae770850f18f10e14d0ab3f", "alt": { - "hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img", + "hash": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643", + "url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img", "size": 5368709120 } }, { "name": "userdata_90", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21.img.xz", - "hash": "3d8a007bae088c5959eb9b82454013f91868946d78380fecea2b1afdfb575c02", - "hash_raw": "f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-02f7abb4b667c04043c0c6950145aaebd704851261f32256d0f7e84a52059dda.img.xz", + "hash": "1eda66d4e31222fc2e792a62ae8e7d322fc643f0b23785e7527bb51a9fee97c7", + "hash_raw": "02f7abb4b667c04043c0c6950145aaebd704851261f32256d0f7e84a52059dda", "size": 96636764160, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "5bfbabb8ff96b149056aa75d5b7e66a7cdd9cb4bcefe23b922c292f7f3a43462" + "ondevice_hash": "679b650ee04b7b1ef610b63fde9b43569fded39ceacf88789b564de99c221ea1" }, { "name": "userdata_89", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf.img.xz", - "hash": "443f136484294b210318842d09fb618d5411c8bdbab9f7421d8c89eb291a8d3f", - "hash_raw": "06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-bab8399bbe3968f3c496f7bc83c2541b33acc1f47814c4ad95801bf5cb7e7588.img.xz", + "hash": "e63d3277285aae1f04fd7f4f48429ce35010f4843ab755f10d360c3aa788e484", + "hash_raw": "bab8399bbe3968f3c496f7bc83c2541b33acc1f47814c4ad95801bf5cb7e7588", "size": 95563022336, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "67db02b29a7e4435951c64cc962a474d048ed444aa912f3494391417cd51a074" + "ondevice_hash": "2947374fc5980ffe3c5b94b61cc1c81bc55214f494153ed234164801731f5dc0" }, { "name": "userdata_30", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6.img.xz", - "hash": "875b580cb786f290a842e9187fd945657561886123eb3075a26f7995a18068f6", - "hash_raw": "06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-22c874b4b66bbc000f3219abede8d62cb307f5786fd526a8473c61422765dea0.img.xz", + "hash": "12d9245711e8c49c51ff2c7b82d7301f2fcb1911edcddb35a105a80911859113", + "hash_raw": "22c874b4b66bbc000f3219abede8d62cb307f5786fd526a8473c61422765dea0", "size": 32212254720, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "16e27ba3c5cf9f0394ce6235ba6021b8a2de293fdb08399f8ca832fa5e4d0b9d" + "ondevice_hash": "03c8b65c945207f887ed6c52d38b53d53d71c8597dcb0b63dfbb11f7cfff8d2b" } ] \ No newline at end of file diff --git a/system/hardware/tici/hardware.h b/system/hardware/tici/hardware.h index ae1087fa7..ed8a7e7d1 100644 --- a/system/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -13,8 +13,6 @@ class HardwareTici : public HardwareNone { public: - static constexpr float MAX_VOLUME = 0.9; - static constexpr float MIN_VOLUME = 0.1; static bool TICI() { return true; } static bool AGNOS() { return true; } static std::string get_os_version() { diff --git a/system/loggerd/encoder/encoder.cc b/system/loggerd/encoder/encoder.cc index b8f2f274d..06922b0cd 100644 --- a/system/loggerd/encoder/encoder.cc +++ b/system/loggerd/encoder/encoder.cc @@ -21,7 +21,7 @@ void VideoEncoder::publisher_publish(int segment_num, uint32_t idx, VisionIpcBuf edata.setFrameId(extra.frame_id); edata.setTimestampSof(extra.timestamp_sof); edata.setTimestampEof(extra.timestamp_eof); - edata.setType(encoder_info.encode_type); + edata.setType(encoder_info.get_settings(in_width).encode_type); edata.setEncodeId(cnt++); edata.setSegmentNum(segment_num); edata.setSegmentId(idx); diff --git a/system/loggerd/encoder/ffmpeg_encoder.cc b/system/loggerd/encoder/ffmpeg_encoder.cc index 091c2fb2d..4d6be4718 100644 --- a/system/loggerd/encoder/ffmpeg_encoder.cc +++ b/system/loggerd/encoder/ffmpeg_encoder.cc @@ -46,7 +46,7 @@ FfmpegEncoder::~FfmpegEncoder() { } void FfmpegEncoder::encoder_open() { - auto codec_id = encoder_info.encode_type == cereal::EncodeIndex::Type::QCAMERA_H264 + auto codec_id = encoder_info.get_settings(in_width).encode_type == cereal::EncodeIndex::Type::QCAMERA_H264 ? AV_CODEC_ID_H264 : AV_CODEC_ID_FFVHUFF; const AVCodec *codec = avcodec_find_encoder(codec_id); diff --git a/system/loggerd/encoder/v4l_encoder.cc b/system/loggerd/encoder/v4l_encoder.cc index ac1da0969..6ee3af13b 100644 --- a/system/loggerd/encoder/v4l_encoder.cc +++ b/system/loggerd/encoder/v4l_encoder.cc @@ -24,14 +24,6 @@ */ const int env_debug_encoder = (getenv("DEBUG_ENCODER") != NULL) ? atoi(getenv("DEBUG_ENCODER")) : 0; -static void checked_ioctl(int fd, unsigned long request, void *argp) { - int ret = util::safe_ioctl(fd, request, argp); - if (ret != 0) { - LOGE("checked_ioctl failed with error %d (%d %lx %p)", errno, fd, request, argp); - assert(0); - } -} - static void dequeue_buffer(int fd, v4l2_buf_type buf_type, unsigned int *index=NULL, unsigned int *bytesused=NULL, unsigned int *flags=NULL, struct timeval *timestamp=NULL) { v4l2_plane plane = {0}; v4l2_buffer v4l_buf = { @@ -40,7 +32,7 @@ static void dequeue_buffer(int fd, v4l2_buf_type buf_type, unsigned int *index=N .m = { .planes = &plane, }, .length = 1, }; - checked_ioctl(fd, VIDIOC_DQBUF, &v4l_buf); + util::safe_ioctl(fd, VIDIOC_DQBUF, &v4l_buf, "VIDIOC_DQBUF failed"); if (index) *index = v4l_buf.index; if (bytesused) *bytesused = v4l_buf.m.planes[0].bytesused; @@ -66,8 +58,7 @@ static void queue_buffer(int fd, v4l2_buf_type buf_type, unsigned int index, Vis .flags = V4L2_BUF_FLAG_TIMESTAMP_COPY, .timestamp = timestamp }; - - checked_ioctl(fd, VIDIOC_QBUF, &v4l_buf); + util::safe_ioctl(fd, VIDIOC_QBUF, &v4l_buf, "VIDIOC_QBUF failed"); } static void request_buffers(int fd, v4l2_buf_type buf_type, unsigned int count) { @@ -76,7 +67,7 @@ static void request_buffers(int fd, v4l2_buf_type buf_type, unsigned int count) .memory = V4L2_MEMORY_USERPTR, .count = count }; - checked_ioctl(fd, VIDIOC_REQBUFS, &reqbuf); + util::safe_ioctl(fd, VIDIOC_REQBUFS, &reqbuf, "VIDIOC_REQBUFS failed"); } void V4LEncoder::dequeue_handler(V4LEncoder *e) { @@ -159,11 +150,14 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei assert(fd >= 0); struct v4l2_capability cap; - checked_ioctl(fd, VIDIOC_QUERYCAP, &cap); + util::safe_ioctl(fd, VIDIOC_QUERYCAP, &cap, "VIDIOC_QUERYCAP failed"); LOGD("opened encoder device %s %s = %d", cap.driver, cap.card, fd); assert(strcmp((const char *)cap.driver, "msm_vidc_driver") == 0); assert(strcmp((const char *)cap.card, "msm_vidc_venc") == 0); + EncoderSettings encoder_settings = encoder_info.get_settings(in_width); + bool is_h265 = encoder_settings.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C; + struct v4l2_format fmt_out = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, .fmt = { @@ -171,13 +165,13 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei // downscales are free with v4l .width = (unsigned int)(out_width), .height = (unsigned int)(out_height), - .pixelformat = (encoder_info.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C) ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264, + .pixelformat = is_h265 ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264, .field = V4L2_FIELD_ANY, .colorspace = V4L2_COLORSPACE_DEFAULT, } } }; - checked_ioctl(fd, VIDIOC_S_FMT, &fmt_out); + util::safe_ioctl(fd, VIDIOC_S_FMT, &fmt_out, "VIDIOC_S_FMT failed"); v4l2_streamparm streamparm = { .type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, @@ -191,7 +185,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei } } }; - checked_ioctl(fd, VIDIOC_S_PARM, &streamparm); + util::safe_ioctl(fd, VIDIOC_S_PARM, &streamparm, "VIDIOC_S_PARM failed"); struct v4l2_format fmt_in = { .type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, @@ -205,7 +199,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei } } }; - checked_ioctl(fd, VIDIOC_S_FMT, &fmt_in); + util::safe_ioctl(fd, VIDIOC_S_FMT, &fmt_in, "VIDIOC_S_FMT failed"); LOGD("in buffer size %d, out buffer size %d", fmt_in.fmt.pix_mp.plane_fmt[0].sizeimage, @@ -214,34 +208,32 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei // shared ctrls { struct v4l2_control ctrls[] = { + { .id = V4L2_CID_MPEG_VIDEO_BITRATE, .value = encoder_settings.bitrate}, + { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = encoder_settings.gop_size - encoder_settings.b_frames - 1}, + { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = encoder_settings.b_frames}, { .id = V4L2_CID_MPEG_VIDEO_HEADER_MODE, .value = V4L2_MPEG_VIDEO_HEADER_MODE_SEPARATE}, - { .id = V4L2_CID_MPEG_VIDEO_BITRATE, .value = encoder_info.bitrate}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_RATE_CONTROL, .value = V4L2_CID_MPEG_VIDC_VIDEO_RATE_CONTROL_VBR_CFR}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_PRIORITY, .value = V4L2_MPEG_VIDC_VIDEO_PRIORITY_REALTIME_DISABLE}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_IDR_PERIOD, .value = 1}, }; for (auto ctrl : ctrls) { - checked_ioctl(fd, VIDIOC_S_CTRL, &ctrl); + util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); } } - if (encoder_info.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C) { + if (is_h265) { struct v4l2_control ctrls[] = { { .id = V4L2_CID_MPEG_VIDC_VIDEO_HEVC_PROFILE, .value = V4L2_MPEG_VIDC_VIDEO_HEVC_PROFILE_MAIN}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_HEVC_TIER_LEVEL, .value = V4L2_MPEG_VIDC_VIDEO_HEVC_LEVEL_HIGH_TIER_LEVEL_5}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_VUI_TIMING_INFO, .value = V4L2_MPEG_VIDC_VIDEO_VUI_TIMING_INFO_ENABLED}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = 29}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = 0}, }; for (auto ctrl : ctrls) { - checked_ioctl(fd, VIDIOC_S_CTRL, &ctrl); + util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); } } else { struct v4l2_control ctrls[] = { { .id = V4L2_CID_MPEG_VIDEO_H264_PROFILE, .value = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH}, { .id = V4L2_CID_MPEG_VIDEO_H264_LEVEL, .value = V4L2_MPEG_VIDEO_H264_LEVEL_UNKNOWN}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = 14}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = 0}, { .id = V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE, .value = V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CABAC}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL, .value = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL_0}, { .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_MODE, .value = 0}, @@ -250,7 +242,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei { .id = V4L2_CID_MPEG_VIDEO_MULTI_SLICE_MODE, .value = 0}, }; for (auto ctrl : ctrls) { - checked_ioctl(fd, VIDIOC_S_CTRL, &ctrl); + util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); } } @@ -260,9 +252,9 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei // start encoder v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMON, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMON, &buf_type, "VIDIOC_STREAMON failed"); buf_type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMON, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMON, &buf_type, "VIDIOC_STREAMON failed"); // queue up output buffers for (unsigned int i = 0; i < BUF_OUT_COUNT; i++) { @@ -305,7 +297,7 @@ void V4LEncoder::encoder_close() { for (int i = 0; i < BUF_IN_COUNT; i++) free_buf_in.push(i); // no frames, stop the encoder struct v4l2_encoder_cmd encoder_cmd = { .cmd = V4L2_ENC_CMD_STOP }; - checked_ioctl(fd, VIDIOC_ENCODER_CMD, &encoder_cmd); + util::safe_ioctl(fd, VIDIOC_ENCODER_CMD, &encoder_cmd, "VIDIOC_ENCODER_CMD failed"); // join waits for V4L2_QCOM_BUF_FLAG_EOS dequeue_handler_thread.join(); assert(extras.empty()); @@ -316,10 +308,10 @@ void V4LEncoder::encoder_close() { V4LEncoder::~V4LEncoder() { encoder_close(); v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMOFF, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMOFF, &buf_type, "VIDIOC_STREAMOFF failed"); request_buffers(fd, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, 0); buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMOFF, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMOFF, &buf_type, "VIDIOC_STREAMOFF failed"); request_buffers(fd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, 0); close(fd); diff --git a/system/loggerd/loggerd.cc b/system/loggerd/loggerd.cc index 144ab9f34..21de1ff33 100644 --- a/system/loggerd/loggerd.cc +++ b/system/loggerd/loggerd.cc @@ -194,7 +194,7 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct return bytes_count; } -void handle_user_flag(LoggerdState *s) { +void handle_preserve_segment(LoggerdState *s) { static int prev_segment = -1; if (s->logger.segment() == prev_segment) return; @@ -222,7 +222,7 @@ void loggerd_thread() { typedef struct ServiceState { std::string name; int counter, freq; - bool encoder, user_flag, record_audio; + bool encoder, preserve_segment, record_audio; } ServiceState; std::unordered_map service_state; std::unordered_map remote_encoders; @@ -246,7 +246,7 @@ void loggerd_thread() { .counter = 0, .freq = it.decimation, .encoder = encoder, - .user_flag = it.name == "userFlag", + .preserve_segment = (it.name == "userBookmark") || (it.name == "audioFeedback"), .record_audio = record_audio, }; } @@ -281,8 +281,8 @@ void loggerd_thread() { if (do_exit) break; ServiceState &service = service_state[sock]; - if (service.user_flag) { - handle_user_flag(&s); + if (service.preserve_segment) { + handle_preserve_segment(&s); } // drain socket diff --git a/system/loggerd/loggerd.h b/system/loggerd/loggerd.h index 5dfb178fd..967caec86 100644 --- a/system/loggerd/loggerd.h +++ b/system/loggerd/loggerd.h @@ -13,10 +13,7 @@ #include "system/loggerd/logger.h" constexpr int MAIN_FPS = 20; -const int MAIN_BITRATE = 1e7; -const int LIVESTREAM_BITRATE = 1e6; -const int QCAM_BITRATE = 256000; - +const auto MAIN_ENCODE_TYPE = Hardware::PC() ? cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS : cereal::EncodeIndex::Type::FULL_H_E_V_C; #define NO_CAMERA_PATIENCE 500 // fall back to time-based rotation if all cameras are dead #define INIT_ENCODE_FUNCTIONS(encode_type) \ @@ -29,6 +26,30 @@ const int SEGMENT_LENGTH = LOGGERD_TEST ? atoi(getenv("LOGGERD_SEGMENT_LENGTH")) constexpr char PRESERVE_ATTR_NAME[] = "user.preserve"; constexpr char PRESERVE_ATTR_VALUE = '1'; + +struct EncoderSettings { + cereal::EncodeIndex::Type encode_type; + int bitrate; + int gop_size; + int b_frames = 0; // we don't use b frames + + static EncoderSettings MainEncoderSettings(int in_width) { + if (in_width <= 1344) { + return EncoderSettings{.encode_type = MAIN_ENCODE_TYPE, .bitrate = 5'000'000, .gop_size = 20}; + } else { + return EncoderSettings{.encode_type = MAIN_ENCODE_TYPE, .bitrate = 10'000'000, .gop_size = 30}; + } + } + + static EncoderSettings QcamEncoderSettings() { + return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = 256'000, .gop_size = 15}; + } + + static EncoderSettings StreamEncoderSettings() { + return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = 1'000'000, .gop_size = 15}; + } +}; + class EncoderInfo { public: const char *publish_name; @@ -39,9 +60,8 @@ public: int frame_width = -1; int frame_height = -1; int fps = MAIN_FPS; - int bitrate = MAIN_BITRATE; - cereal::EncodeIndex::Type encode_type = Hardware::PC() ? cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS - : cereal::EncodeIndex::Type::FULL_H_E_V_C; + std::function get_settings; + ::cereal::EncodeData::Reader (cereal::Event::Reader::*get_encode_data_func)() const; void (cereal::Event::Builder::*set_encode_idx_func)(::cereal::EncodeIndex::Reader); cereal::EncodeData::Builder (cereal::Event::Builder::*init_encode_data_func)(); @@ -59,12 +79,14 @@ const EncoderInfo main_road_encoder_info = { .publish_name = "roadEncodeData", .thumbnail_name = "thumbnail", .filename = "fcamera.hevc", + .get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);}, INIT_ENCODE_FUNCTIONS(RoadEncode), }; const EncoderInfo main_wide_road_encoder_info = { .publish_name = "wideRoadEncodeData", .filename = "ecamera.hevc", + .get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);}, INIT_ENCODE_FUNCTIONS(WideRoadEncode), }; @@ -72,39 +94,36 @@ const EncoderInfo main_driver_encoder_info = { .publish_name = "driverEncodeData", .filename = "dcamera.hevc", .record = Params().getBool("RecordFront"), + .get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);}, INIT_ENCODE_FUNCTIONS(DriverEncode), }; const EncoderInfo stream_road_encoder_info = { .publish_name = "livestreamRoadEncodeData", //.thumbnail_name = "thumbnail", - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .record = false, - .bitrate = LIVESTREAM_BITRATE, + .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamRoadEncode), }; const EncoderInfo stream_wide_road_encoder_info = { .publish_name = "livestreamWideRoadEncodeData", - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .record = false, - .bitrate = LIVESTREAM_BITRATE, + .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamWideRoadEncode), }; const EncoderInfo stream_driver_encoder_info = { .publish_name = "livestreamDriverEncodeData", - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .record = false, - .bitrate = LIVESTREAM_BITRATE, + .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamDriverEncode), }; const EncoderInfo qcam_encoder_info = { .publish_name = "qRoadEncodeData", .filename = "qcamera.ts", - .bitrate = QCAM_BITRATE, - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, + .get_settings = [](int){return EncoderSettings::QcamEncoderSettings();}, .frame_width = 526, .frame_height = 330, .include_audio = Params().getBool("RecordAudio"), diff --git a/system/loggerd/tests/test_encoder.py b/system/loggerd/tests/test_encoder.py index b24bfbe16..e4dabd3df 100644 --- a/system/loggerd/tests/test_encoder.py +++ b/system/loggerd/tests/test_encoder.py @@ -19,11 +19,12 @@ from openpilot.system.hardware.hw import Paths SEGMENT_LENGTH = 2 FULL_SIZE = 2507572 +def hevc_size(w): return FULL_SIZE // 2 if w <= 1344 else FULL_SIZE CAMERAS = [ - ("fcamera.hevc", 20, FULL_SIZE, "roadEncodeIdx"), - ("dcamera.hevc", 20, FULL_SIZE, "driverEncodeIdx"), - ("ecamera.hevc", 20, FULL_SIZE, "wideRoadEncodeIdx"), - ("qcamera.ts", 20, 130000, None), + ("fcamera.hevc", 20, hevc_size, "roadEncodeIdx"), + ("dcamera.hevc", 20, hevc_size, "driverEncodeIdx"), + ("ecamera.hevc", 20, hevc_size, "wideRoadEncodeIdx"), + ("qcamera.ts", 20, lambda x: 130000, None), ] # we check frame count, so we don't have to be too strict on size @@ -76,7 +77,7 @@ class TestEncoder: # check each camera file size counts = [] first_frames = [] - for camera, fps, size, encode_idx_name in CAMERAS: + for camera, fps, size_lambda, encode_idx_name in CAMERAS: if not record_front and "dcamera" in camera: continue @@ -86,14 +87,14 @@ class TestEncoder: assert os.path.exists(file_path), f"segment #{i}: '{file_path}' missing" # TODO: this ffprobe call is really slow - # check frame count - cmd = f"ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 {file_path}" + # get width and check frame count + cmd = f"ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets,width -of csv=p=0 {file_path}" if TICI: cmd = "LD_LIBRARY_PATH=/usr/local/lib " + cmd expected_frames = fps * SEGMENT_LENGTH - probe = subprocess.check_output(cmd, shell=True, encoding='utf8') - frame_count = int(probe.split('\n')[0].strip()) + probe = subprocess.check_output(cmd, shell=True, encoding='utf8').split('\n')[0].strip().split(',') + frame_width, frame_count = int(probe[0]), int(probe[1]) counts.append(frame_count) assert frame_count == expected_frames, \ @@ -101,8 +102,9 @@ class TestEncoder: # sanity check file size file_size = os.path.getsize(file_path) - assert math.isclose(file_size, size, rel_tol=FILE_SIZE_TOLERANCE), \ - f"{file_path} size {file_size} isn't close to target size {size}" + target_size = size_lambda(frame_width) + assert math.isclose(file_size, target_size, rel_tol=FILE_SIZE_TOLERANCE), \ + f"{file_path} size {file_size} isn't close to target size {target_size}" # Check encodeIdx if encode_idx_name is not None: diff --git a/system/loggerd/tests/test_loggerd.py b/system/loggerd/tests/test_loggerd.py index 8f9d92e10..c6a4b12e6 100644 --- a/system/loggerd/tests/test_loggerd.py +++ b/system/loggerd/tests/test_loggerd.py @@ -282,15 +282,15 @@ class TestLoggerd: sent.clear_write_flag() assert sent.to_bytes() == m.as_builder().to_bytes() - def test_preserving_flagged_segments(self): - services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) | {"userFlag"} + def test_preserving_bookmarked_segments(self): + services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) | {"userBookmark"} self._publish_random_messages(services) segment_dir = self._get_latest_log_dir() assert getxattr(segment_dir, PRESERVE_ATTR_NAME) == PRESERVE_ATTR_VALUE - def test_not_preserving_unflagged_segments(self): - services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) - {"userFlag"} + def test_not_preserving_nonbookmarked_segments(self): + services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) - {"userBookmark", "audioFeedback"} self._publish_random_messages(services) segment_dir = self._get_latest_log_dir() diff --git a/system/manager/manager.py b/system/manager/manager.py index a567c30f8..d1b684aa7 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -3,6 +3,7 @@ import datetime import os import signal import sys +import time import traceback from cereal import log @@ -165,6 +166,14 @@ def manager_thread() -> None: msg.managerState.processes = [p.get_process_state_msg() for p in managed_processes.values()] pm.send('managerState', msg) + # kick AGNOS power monitoring watchdog + try: + if sm.all_checks(['deviceState']): + with open("/var/tmp/power_watchdog", "w") as f: + f.write(str(time.monotonic())) + except Exception: + pass + # Exit main loop when uninstall/shutdown/reboot is needed shutdown = False for param in ("DoUninstall", "DoShutdown", "DoReboot"): diff --git a/system/manager/process.py b/system/manager/process.py index c83cc46e0..5e86e87c7 100644 --- a/system/manager/process.py +++ b/system/manager/process.py @@ -270,7 +270,7 @@ class DaemonProcess(ManagerProcess): stderr=open('/dev/null', 'w'), preexec_fn=os.setpgrp) - self.params.put(self.param_name, str(proc.pid)) + self.params.put(self.param_name, proc.pid) def stop(self, retry=True, block=True, sig=None) -> None: pass diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 6cdb89d98..c90565180 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -117,6 +117,7 @@ procs = [ PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC), NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)), + PythonProcess("raylib_ui", "selfdrive.ui.ui", always_run, enabled=False, watchdog_max_dt=(5 if not PC else None)), PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad), PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad), NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False), @@ -142,6 +143,7 @@ procs = [ PythonProcess("updated", "system.updated.updated", only_offroad, enabled=not PC), PythonProcess("uploader", "system.loggerd.uploader", always_run), PythonProcess("statsd", "system.statsd", always_run), + PythonProcess("feedbackd", "selfdrive.ui.feedback.feedbackd", only_onroad), # debug procs NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar), diff --git a/system/sensord/sensord.py b/system/sensord/sensord.py index ed9d0ed41..2b6467fa7 100755 --- a/system/sensord/sensord.py +++ b/system/sensord/sensord.py @@ -98,6 +98,13 @@ def main() -> None: (MMC5603NJ_Magn(I2C_BUS_IMU), "magnetometer", False), ] + # Reset sensors + for sensor, _, _ in sensors_cfg: + try: + sensor.reset() + except Exception: + cloudlog.exception(f"Error initializing {sensor} sensor") + # Initialize sensors exit_event = threading.Event() threads = [ diff --git a/system/sensord/sensors/i2c_sensor.py b/system/sensord/sensors/i2c_sensor.py index 0e15a6622..336ebb1fd 100644 --- a/system/sensord/sensors/i2c_sensor.py +++ b/system/sensord/sensors/i2c_sensor.py @@ -40,6 +40,11 @@ class Sensor: def device_address(self) -> int: raise NotImplementedError + def reset(self) -> None: + # optional. + # not part of init due to shared registers + pass + def init(self) -> None: raise NotImplementedError diff --git a/system/sensord/sensors/lsm6ds3_accel.py b/system/sensord/sensors/lsm6ds3_accel.py index 2d788fcbe..43863daa9 100644 --- a/system/sensord/sensors/lsm6ds3_accel.py +++ b/system/sensord/sensors/lsm6ds3_accel.py @@ -31,6 +31,10 @@ class LSM6DS3_Accel(Sensor): def device_address(self) -> int: return 0x6A + def reset(self): + self.write(0x12, 0x1) + time.sleep(0.1) + def init(self): chip_id = self.verify_chip_id(0x0F, [0x69, 0x6A]) if chip_id == 0x6A: diff --git a/system/sensord/sensors/lsm6ds3_gyro.py b/system/sensord/sensors/lsm6ds3_gyro.py index 68fd267df..60de2bbe0 100644 --- a/system/sensord/sensors/lsm6ds3_gyro.py +++ b/system/sensord/sensors/lsm6ds3_gyro.py @@ -29,6 +29,10 @@ class LSM6DS3_Gyro(Sensor): def device_address(self) -> int: return 0x6A + def reset(self): + self.write(0x12, 0x1) + time.sleep(0.1) + def init(self): chip_id = self.verify_chip_id(0x0F, [0x69, 0x6A]) if chip_id == 0x6A: diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index c7fe39adf..718bf036f 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -19,6 +19,7 @@ FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions MOUSE_THREAD_RATE = 140 # touch controller runs at 140Hz +MAX_TOUCH_SLOTS = 2 ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "0") == "1" SHOW_FPS = os.getenv("SHOW_FPS") == "1" @@ -58,6 +59,7 @@ class MousePos(NamedTuple): class MouseEvent(NamedTuple): pos: MousePos + slot: int left_pressed: bool left_released: bool left_down: bool @@ -67,7 +69,7 @@ class MouseEvent(NamedTuple): class MouseState: def __init__(self): self._events: deque[MouseEvent] = deque(maxlen=MOUSE_THREAD_RATE) # bound event list - self._prev_mouse_event: MouseEvent | None = None + self._prev_mouse_event: list[MouseEvent | None] = [None] * MAX_TOUCH_SLOTS self._rk = Ratekeeper(MOUSE_THREAD_RATE) self._lock = threading.Lock() @@ -98,19 +100,21 @@ class MouseState: self._rk.keep_time() def _handle_mouse_event(self): - mouse_pos = rl.get_mouse_position() - ev = MouseEvent( - MousePos(mouse_pos.x, mouse_pos.y), - rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT), - rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT), - rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT), - time.monotonic(), - ) - # Only add changes - if self._prev_mouse_event is None or ev[:-1] != self._prev_mouse_event[:-1]: - with self._lock: - self._events.append(ev) - self._prev_mouse_event = ev + for slot in range(MAX_TOUCH_SLOTS): + mouse_pos = rl.get_touch_position(slot) + ev = MouseEvent( + MousePos(mouse_pos.x, mouse_pos.y), + slot, + rl.is_mouse_button_pressed(slot), + rl.is_mouse_button_released(slot), + rl.is_mouse_button_down(slot), + time.monotonic(), + ) + # Only add changes + if self._prev_mouse_event[slot] is None or ev[:-1] != self._prev_mouse_event[slot][:-1]: + with self._lock: + self._events.append(ev) + self._prev_mouse_event[slot] = ev class GuiApplication: diff --git a/system/ui/lib/emoji.py b/system/ui/lib/emoji.py new file mode 100644 index 000000000..28139158a --- /dev/null +++ b/system/ui/lib/emoji.py @@ -0,0 +1,47 @@ +import io +import re + +from PIL import Image, ImageDraw, ImageFont +import pyray as rl + +_cache: dict[str, rl.Texture] = {} + +EMOJI_REGEX = re.compile( +"""[\U0001F600-\U0001F64F +\U0001F300-\U0001F5FF +\U0001F680-\U0001F6FF +\U0001F1E0-\U0001F1FF +\U00002700-\U000027BF +\U0001F900-\U0001F9FF +\U00002600-\U000026FF +\U00002300-\U000023FF +\U00002B00-\U00002BFF +\U0001FA70-\U0001FAFF +\U0001F700-\U0001F77F +\u2640-\u2642 +\u2600-\u2B55 +\u200d +\u23cf +\u23e9 +\u231a +\ufe0f +\u3030 +]+""", + flags=re.UNICODE +) + +def find_emoji(text): + return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)] + +def emoji_tex(emoji): + if emoji not in _cache: + img = Image.new("RGBA", (128, 128), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + font = ImageFont.truetype("NotoColorEmoji", 109) + draw.text((0, 0), emoji, font=font, embedded_color=True) + buffer = io.BytesIO() + img.save(buffer, format="PNG") + l = buffer.tell() + buffer.seek(0) + _cache[emoji] = rl.load_texture_from_image(rl.load_image_from_memory(".png", buffer.getvalue(), l)) + return _cache[emoji] diff --git a/system/ui/lib/scroll_panel.py b/system/ui/lib/scroll_panel.py index 21b6795a7..e2296fd5e 100644 --- a/system/ui/lib/scroll_panel.py +++ b/system/ui/lib/scroll_panel.py @@ -41,8 +41,9 @@ class GuiScrollPanel: def handle_scroll(self, bounds: rl.Rectangle, content: rl.Rectangle) -> rl.Vector2: # TODO: HACK: this class is driven by mouse events, so we need to ensure we have at least one event to process - for mouse_event in gui_app.mouse_events or [MouseEvent(MousePos(0, 0), False, False, False, time.monotonic())]: - self._handle_mouse_event(mouse_event, bounds, content) + for mouse_event in gui_app.mouse_events or [MouseEvent(MousePos(0, 0), 0, False, False, False, time.monotonic())]: + if mouse_event.slot == 0: + self._handle_mouse_event(mouse_event, bounds, content) return self._offset def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, content: rl.Rectangle): diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index e4ee224d5..5d0f84bc8 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -441,33 +441,33 @@ class WifiManager: settings_iface.on_connection_removed(self._on_connection_removed) def _on_properties_changed(self, interface: str, changed: dict, invalidated: list): - # print("property changed", interface, changed, invalidated) - if 'LastScan' in changed: + if interface == NM_WIRELESS_IFACE and 'LastScan' in changed: asyncio.create_task(self._refresh_networks()) elif interface == NM_WIRELESS_IFACE and "ActiveAccessPoint" in changed: new_ap_path = changed["ActiveAccessPoint"].value if self.active_ap_path != new_ap_path: self.active_ap_path = new_ap_path - asyncio.create_task(self._refresh_networks()) def _on_state_changed(self, new_state: int, old_state: int, reason: int): - print("State changed", new_state, old_state, reason) if new_state == NMDeviceState.ACTIVATED: if self.callbacks.activated: self.callbacks.activated() - asyncio.create_task(self._refresh_networks()) self._current_connection_ssid = None + asyncio.create_task(self._refresh_networks()) elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH): for network in self.networks: network.is_connected = False + # BAD PASSWORD if new_state == NMDeviceState.NEED_AUTH and reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and self.callbacks.need_auth: if self._current_connection_ssid: + asyncio.create_task(self.forget_connection(self._current_connection_ssid)) self.callbacks.need_auth(self._current_connection_ssid) else: # Try to find the network from active_ap_path for network in self.networks: if network.path == self.active_ap_path: + asyncio.create_task(self.forget_connection(network.ssid)) self.callbacks.need_auth(network.ssid) break else: @@ -486,9 +486,6 @@ class WifiManager: if self.callbacks.forgotten: self.callbacks.forgotten(ssid) - - # Update network list to reflect the removed saved connection - asyncio.create_task(self._refresh_networks()) break async def _add_saved_connection(self, path: str) -> None: @@ -497,14 +494,13 @@ class WifiManager: settings = await self._get_connection_settings(path) if ssid := self._extract_ssid(settings): self.saved_connections[ssid] = path - await self._refresh_networks() except DBusError as e: cloudlog.error(f"Failed to add connection {path}: {e}") def _extract_ssid(self, settings: dict) -> str | None: """Extract SSID from connection settings.""" ssid_variant = settings.get('802-11-wireless', {}).get('ssid', Variant('ay', b'')).value - return ''.join(chr(b) for b in ssid_variant) if ssid_variant else None + return bytes(ssid_variant).decode('utf-8') if ssid_variant else None async def _add_match_rule(self, rule): """Add a match rule on the bus.""" @@ -534,7 +530,7 @@ class WifiManager: props_iface = await self._get_interface(NM, ap_path, NM_PROPERTIES_IFACE) properties = await props_iface.call_get_all('org.freedesktop.NetworkManager.AccessPoint') ssid_variant = properties['Ssid'].value - ssid = ''.join(chr(byte) for byte in ssid_variant) + ssid = bytes(ssid_variant).decode('utf-8') if not ssid: continue @@ -543,18 +539,28 @@ class WifiManager: flags = properties['Flags'].value wpa_flags = properties['WpaFlags'].value rsn_flags = properties['RsnFlags'].value - existing_network = network_dict.get(ssid) - if not existing_network or ((not existing_network.bssid and bssid) or (existing_network.strength < strength)): + + # May be multiple access points for each SSID. Use first for ssid + # and security type, then update the rest using all APs + if ssid not in network_dict: network_dict[ssid] = NetworkInfo( ssid=ssid, - strength=strength, + strength=0, security_type=self._get_security_type(flags, wpa_flags, rsn_flags), - path=ap_path, - bssid=bssid, - is_connected=self.active_ap_path == ap_path and self._current_connection_ssid != ssid, + path="", + bssid="", + is_connected=False, is_saved=ssid in self.saved_connections ) + existing_network = network_dict.get(ssid) + if existing_network.strength < strength: + existing_network.strength = strength + existing_network.path = ap_path + existing_network.bssid = bssid + if self.active_ap_path == ap_path: + existing_network.is_connected = self._current_connection_ssid != ssid + except DBusError as e: cloudlog.error(f"Error fetching networks: {e}") except Exception as e: diff --git a/system/ui/reset.py b/system/ui/reset.py index 7c9500c7c..b1bf5a6b6 100755 --- a/system/ui/reset.py +++ b/system/ui/reset.py @@ -87,13 +87,15 @@ class Reset(Widget): button_width = (rect.width - button_spacing) / 2.0 if self._reset_state != ResetState.RESETTING: - if self._mode == ResetMode.RECOVER or self._reset_state == ResetState.FAILED: - self._reboot_button.render(rl.Rectangle(rect.x, button_top, rect.width, button_height)) + if self._mode == ResetMode.RECOVER: + self._reboot_button.render(rl.Rectangle(rect.x, button_top, button_width, button_height)) elif self._mode == ResetMode.USER_RESET: self._cancel_button.render(rl.Rectangle(rect.x, button_top, button_width, button_height)) if self._reset_state != ResetState.FAILED: self._confirm_button.render(rl.Rectangle(rect.x + button_width + 50, button_top, button_width, button_height)) + else: + self._reboot_button.render(rl.Rectangle(rect.x, button_top, rect.width, button_height)) return self._render_status @@ -123,7 +125,7 @@ def main(): elif sys.argv[1] == "--format": mode = ResetMode.FORMAT - gui_app.init_window("System Reset") + gui_app.init_window("System Reset", 20) reset = Reset(mode) if mode == ResetMode.FORMAT: diff --git a/system/ui/setup.py b/system/ui/setup.py index 060d8cf4e..d675e868f 100755 --- a/system/ui/setup.py +++ b/system/ui/setup.py @@ -4,6 +4,7 @@ import re import threading import time import urllib.request +from urllib.parse import urlparse from enum import IntEnum import pyray as rl @@ -11,9 +12,9 @@ from cereal import log from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio from openpilot.system.ui.widgets.keyboard import Keyboard -from openpilot.system.ui.widgets.label import gui_label, gui_text_box +from openpilot.system.ui.widgets.label import Label, TextAlignment from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper NetworkType = log.DeviceState.NetworkType @@ -35,9 +36,10 @@ class SetupState(IntEnum): GETTING_STARTED = 1 NETWORK_SETUP = 2 SOFTWARE_SELECTION = 3 - CUSTOM_URL = 4 + CUSTOM_SOFTWARE = 4 DOWNLOADING = 5 DOWNLOAD_FAILED = 6 + CUSTOM_SOFTWARE_WARNING = 7 class Setup(Widget): @@ -57,10 +59,50 @@ class Setup(Widget): self.wifi_ui = WifiManagerUI(self.wifi_manager) self.keyboard = Keyboard() self.selected_radio = None - self.warning = gui_app.texture("icons/warning.png", 150, 150) self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100) + self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, TextAlignment.LEFT, text_color=rl.Color(255, 89, 79, 255)) + self._low_voltage_body_label = Label("Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE, + text_alignment=TextAlignment.LEFT) + self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback) + self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown) + + self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0) + self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + self._getting_started_body_label = Label("Before we get on the road, let's finish installation and cover some details.", + BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT) + + self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) + self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) + self._software_selection_continue_button = Button("Continue", self._software_selection_continue_button_callback, + button_style=ButtonStyle.PRIMARY) + self._software_selection_continue_button.set_enabled(False) + self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback) + self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + + self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot) + self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY) + self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + self._download_failed_url_label = Label("", 64, FontWeight.NORMAL, TextAlignment.LEFT) + self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT) + + self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback) + self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback, + button_style=ButtonStyle.PRIMARY) + self._network_setup_continue_button.set_enabled(False) + self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + + self._custom_software_warning_continue_button = Button("Continue", self._custom_software_warning_continue_button_callback) + self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback) + self._custom_software_warning_title_label = Label("WARNING: Custom Software", 100, FontWeight.BOLD, TextAlignment.LEFT, text_color=rl.Color(255,89,79,255), + text_padding=60) + self._custom_software_warning_body_label = Label("Use caution when installing third-party software. Third-party software has not been tested by comma," + + " and may cause damage to your device and/or vehicle.\n\nIf you'd like to proceed, use https://flash.comma.ai " + + "to restore your device to a factory state later.", + 85, text_alignment=TextAlignment.LEFT, text_padding=60) + self._downloading_body_label = Label("Downloading...", TITLE_FONT_SIZE, FontWeight.MEDIUM) + try: with open("/sys/class/hwmon/hwmon1/in1_input") as f: voltage = float(f.read().strip()) / 1000.0 @@ -78,48 +120,70 @@ class Setup(Widget): self.render_network_setup(rect) elif self.state == SetupState.SOFTWARE_SELECTION: self.render_software_selection(rect) - elif self.state == SetupState.CUSTOM_URL: - self.render_custom_url() + elif self.state == SetupState.CUSTOM_SOFTWARE_WARNING: + self.render_custom_software_warning(rect) + elif self.state == SetupState.CUSTOM_SOFTWARE: + self.render_custom_software() elif self.state == SetupState.DOWNLOADING: self.render_downloading(rect) elif self.state == SetupState.DOWNLOAD_FAILED: self.render_download_failed(rect) + def _low_voltage_continue_button_callback(self): + self.state = SetupState.GETTING_STARTED + + def _custom_software_warning_back_button_callback(self): + self.state = SetupState.SOFTWARE_SELECTION + + def _custom_software_warning_continue_button_callback(self): + self.state = SetupState.CUSTOM_SOFTWARE + + def _getting_started_button_callback(self): + self.state = SetupState.NETWORK_SETUP + self.stop_network_check_thread.clear() + self.start_network_check() + + def _software_selection_back_button_callback(self): + self.state = SetupState.NETWORK_SETUP + self.stop_network_check_thread.clear() + self.start_network_check() + + def _software_selection_continue_button_callback(self): + if self._software_selection_openpilot_button.selected: + self.download(OPENPILOT_URL) + else: + self.state = SetupState.CUSTOM_SOFTWARE_WARNING + + def _download_failed_startover_button_callback(self): + self.state = SetupState.GETTING_STARTED + + def _network_setup_back_button_callback(self): + self.state = SetupState.GETTING_STARTED + + def _network_setup_continue_button_callback(self): + self.state = SetupState.SOFTWARE_SELECTION + self.stop_network_check_thread.set() + def render_low_voltage(self, rect: rl.Rectangle): rl.draw_texture(self.warning, int(rect.x + 150), int(rect.y + 110), rl.WHITE) - title_rect = rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100, rect.width - 500 - 150, TITLE_FONT_SIZE) - gui_label(title_rect, "WARNING: Low Voltage", TITLE_FONT_SIZE, rl.Color(255, 89, 79, 255), FontWeight.MEDIUM) - - body_rect = rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100 + TITLE_FONT_SIZE + 25, rect.width - 500 - 150, BODY_FONT_SIZE * 3) - gui_text_box(body_rect, "Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE) + self._low_voltage_title_label.render(rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100, rect.width - 500 - 150, TITLE_FONT_SIZE)) + self._low_voltage_body_label.render(rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 150, rect.width - 500, BODY_FONT_SIZE * 3)) button_width = (rect.width - MARGIN * 3) / 2 button_y = rect.height - MARGIN - BUTTON_HEIGHT - - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Power off"): - HARDWARE.shutdown() - - if gui_button(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT), "Continue"): - self.state = SetupState.GETTING_STARTED + self._low_voltage_poweroff_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._low_voltage_continue_button.render(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT)) def render_getting_started(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + 165, rect.y + 280, rect.width - 265, TITLE_FONT_SIZE) - gui_label(title_rect, "Getting Started", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) - - desc_rect = rl.Rectangle(rect.x + 165, rect.y + 280 + TITLE_FONT_SIZE + 90, rect.width - 500, BODY_FONT_SIZE * 3) - gui_text_box(desc_rect, "Before we get on the road, let's finish installation and cover some details.", BODY_FONT_SIZE) + self._getting_started_title_label.render(rl.Rectangle(rect.x + 165, rect.y + 280, rect.width - 265, TITLE_FONT_SIZE)) + self._getting_started_body_label.render(rl.Rectangle(rect.x + 165, rect.y + 280 + TITLE_FONT_SIZE, rect.width - 500, BODY_FONT_SIZE * 3)) btn_rect = rl.Rectangle(rect.width - NEXT_BUTTON_WIDTH, 0, NEXT_BUTTON_WIDTH, rect.height) - - ret = gui_button(btn_rect, "", button_style=ButtonStyle.PRIMARY, border_radius=0) + self._getting_started_button.render(btn_rect) triangle = gui_app.texture("images/button_continue_triangle.png", 54, int(btn_rect.height)) rl.draw_texture_v(triangle, rl.Vector2(btn_rect.x + btn_rect.width / 2 - triangle.width / 2, btn_rect.height / 2 - triangle.height / 2), rl.WHITE) - if ret: - self.state = SetupState.NETWORK_SETUP - self.start_network_check() - def check_network_connectivity(self): while not self.stop_network_check_thread.is_set(): if self.state == SetupState.NETWORK_SETUP: @@ -145,8 +209,7 @@ class Setup(Widget): self.network_check_thread.join() def render_network_setup(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE) - gui_label(title_rect, "Connect to Wi-Fi", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) + self._network_setup_title_label.render(rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE)) wifi_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN + 25, rect.width - MARGIN * 2, rect.height - TITLE_FONT_SIZE - 25 - BUTTON_HEIGHT - MARGIN * 3) @@ -157,102 +220,69 @@ class Setup(Widget): button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 button_y = rect.height - BUTTON_HEIGHT - MARGIN - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Back"): - self.state = SetupState.GETTING_STARTED + self._network_setup_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) # Check network connectivity status continue_enabled = self.network_connected.is_set() + self._network_setup_continue_button.set_enabled(continue_enabled) continue_text = ("Continue" if self.wifi_connected.is_set() else "Continue without Wi-Fi") if continue_enabled else "Waiting for internet" - - if gui_button( - rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), - continue_text, - button_style=ButtonStyle.PRIMARY if continue_enabled else ButtonStyle.NORMAL, - is_enabled=continue_enabled, - ): - self.state = SetupState.SOFTWARE_SELECTION - self.stop_network_check_thread.set() + self._network_setup_continue_button.set_text(continue_text) + self._network_setup_continue_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT)) def render_software_selection(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE) - gui_label(title_rect, "Choose Software to Install", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) + self._software_selection_title_label.render(rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE)) radio_height = 230 radio_spacing = 30 + self._software_selection_continue_button.set_enabled(False) + openpilot_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2, rect.width - MARGIN * 2, radio_height) - openpilot_selected = self.selected_radio == "openpilot" + self._software_selection_openpilot_button.render(openpilot_rect) - rl.draw_rectangle_rounded(openpilot_rect, 0.1, 10, rl.Color(70, 91, 234, 255) if openpilot_selected else rl.Color(79, 79, 79, 255)) - gui_label(rl.Rectangle(openpilot_rect.x + 100, openpilot_rect.y, openpilot_rect.width - 200, radio_height), "openpilot", BODY_FONT_SIZE) - - if openpilot_selected: - checkmark_pos = rl.Vector2(openpilot_rect.x + openpilot_rect.width - 100 - self.checkmark.width, - openpilot_rect.y + radio_height / 2 - self.checkmark.height / 2) - rl.draw_texture_v(self.checkmark, checkmark_pos, rl.WHITE) + if self._software_selection_openpilot_button.selected: + self._software_selection_continue_button.set_enabled(True) + self._software_selection_custom_software_button.selected = False custom_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2 + radio_height + radio_spacing, rect.width - MARGIN * 2, radio_height) - custom_selected = self.selected_radio == "custom" + self._software_selection_custom_software_button.render(custom_rect) - rl.draw_rectangle_rounded(custom_rect, 0.1, 10, rl.Color(70, 91, 234, 255) if custom_selected else rl.Color(79, 79, 79, 255)) - gui_label(rl.Rectangle(custom_rect.x + 100, custom_rect.y, custom_rect.width - 200, radio_height), "Custom Software", BODY_FONT_SIZE) - - if custom_selected: - checkmark_pos = rl.Vector2(custom_rect.x + custom_rect.width - 100 - self.checkmark.width, custom_rect.y + radio_height / 2 - self.checkmark.height / 2) - rl.draw_texture_v(self.checkmark, checkmark_pos, rl.WHITE) - - mouse_pos = rl.get_mouse_position() - if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): - if rl.check_collision_point_rec(mouse_pos, openpilot_rect): - self.selected_radio = "openpilot" - elif rl.check_collision_point_rec(mouse_pos, custom_rect): - self.selected_radio = "custom" + if self._software_selection_custom_software_button.selected: + self._software_selection_continue_button.set_enabled(True) + self._software_selection_openpilot_button.selected = False button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 button_y = rect.height - BUTTON_HEIGHT - MARGIN - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Back"): - self.state = SetupState.NETWORK_SETUP - - continue_enabled = self.selected_radio is not None - if gui_button( - rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), - "Continue", - button_style=ButtonStyle.PRIMARY, - is_enabled=continue_enabled, - ): - if continue_enabled: - if self.selected_radio == "openpilot": - self.download(OPENPILOT_URL) - else: - self.state = SetupState.CUSTOM_URL + self._software_selection_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._software_selection_continue_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT)) def render_downloading(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x, rect.y + rect.height / 2 - TITLE_FONT_SIZE / 2, rect.width, TITLE_FONT_SIZE) - gui_label(title_rect, "Downloading...", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + self._downloading_body_label.render(rl.Rectangle(rect.x, rect.y + rect.height / 2 - TITLE_FONT_SIZE / 2, rect.width, TITLE_FONT_SIZE)) def render_download_failed(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + 117, rect.y + 185, rect.width - 117, TITLE_FONT_SIZE) - gui_label(title_rect, "Download Failed", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) + self._download_failed_title_label.render(rl.Rectangle(rect.x + 117, rect.y + 185, rect.width - 117, TITLE_FONT_SIZE)) + self._download_failed_url_label.set_text(self.failed_url) + self._download_failed_url_label.render(rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67, rect.width - 117 - 100, 64)) - url_rect = rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67, rect.width - 117 - 100, 64) - gui_label(url_rect, self.failed_url, 64, font_weight=FontWeight.NORMAL) - - error_rect = rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67 + 64 + 48, - rect.width - 117 - 100, rect.height - 185 + TITLE_FONT_SIZE + 67 + 64 + 48 - BUTTON_HEIGHT - MARGIN * 2) - gui_text_box(error_rect, self.failed_reason, BODY_FONT_SIZE) + self._download_failed_body_label.set_text(self.failed_reason) + self._download_failed_body_label.render(rl.Rectangle(rect.x + 117, rect.y, rect.width - 117 - 100, rect.height)) button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 button_y = rect.height - BUTTON_HEIGHT - MARGIN + self._download_failed_reboot_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._download_failed_startover_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT)) - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Reboot device"): - HARDWARE.reboot() + def render_custom_software_warning(self, rect: rl.Rectangle): + self._custom_software_warning_title_label.render(rl.Rectangle(rect.x + 50, rect.y + 150, rect.width - 265, TITLE_FONT_SIZE)) + self._custom_software_warning_body_label.render(rl.Rectangle(rect.x + 50, rect.y + 200 , rect.width - 50, BODY_FONT_SIZE * 3)) - if gui_button(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), "Start over", - button_style=ButtonStyle.PRIMARY): - self.state = SetupState.GETTING_STARTED + button_width = (rect.width - MARGIN * 3) / 2 + button_y = rect.height - MARGIN - BUTTON_HEIGHT + self._custom_software_warning_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._custom_software_warning_continue_button.render(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT)) - def render_custom_url(self): + def render_custom_software(self): def handle_keyboard_result(result): # Enter pressed if result == 1: @@ -265,6 +295,7 @@ class Setup(Widget): elif result == 0: self.state = SetupState.SOFTWARE_SELECTION + self.keyboard.reset() self.keyboard.set_title("Enter URL", "for Custom Software") gui_app.set_modal_overlay(self.keyboard, callback=handle_keyboard_result) @@ -273,7 +304,9 @@ class Setup(Widget): if re.match("^([^/.]+)/([^/]+)$", url): url = f"https://installer.comma.ai/{url}" - self.download_url = url + parsed = urlparse(url, scheme='https') + self.download_url = (urlparse(f"https://{url}") if not parsed.netloc else parsed).geturl() + self.state = SetupState.DOWNLOADING self.download_thread = threading.Thread(target=self._download_thread, daemon=True) @@ -333,7 +366,7 @@ class Setup(Widget): def main(): try: - gui_app.init_window("Setup") + gui_app.init_window("Setup", 20) setup = Setup() for _ in gui_app.render(): setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 7a436b8e9..d45f48ac3 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -2,7 +2,7 @@ import abc import pyray as rl from enum import IntEnum from collections.abc import Callable -from openpilot.system.ui.lib.application import gui_app, MousePos +from openpilot.system.ui.lib.application import gui_app, MousePos, MAX_TOUCH_SLOTS class DialogResult(IntEnum): @@ -15,29 +15,18 @@ class Widget(abc.ABC): def __init__(self): self._rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) self._parent_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) - self._is_pressed = False + self._is_pressed = [False] * MAX_TOUCH_SLOTS + # if current mouse/touch down started within the widget's rectangle + self._tracking_is_pressed = [False] * MAX_TOUCH_SLOTS + self._enabled: bool | Callable[[], bool] = True self._is_visible: bool | Callable[[], bool] = True self._touch_valid_callback: Callable[[], bool] | None = None - - def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: - """Set a callback to determine if the widget can be clicked.""" - self._touch_valid_callback = touch_callback - - def _touch_valid(self) -> bool: - """Check if the widget can be touched.""" - return self._touch_valid_callback() if self._touch_valid_callback else True - - @property - def is_visible(self) -> bool: - return self._is_visible() if callable(self._is_visible) else self._is_visible + self._multi_touch = False @property def rect(self) -> rl.Rectangle: return self._rect - def set_visible(self, visible: bool | Callable[[], bool]) -> None: - self._is_visible = visible - def set_rect(self, rect: rl.Rectangle) -> None: changed = (self._rect.x != rect.x or self._rect.y != rect.y or self._rect.width != rect.width or self._rect.height != rect.height) @@ -49,6 +38,32 @@ class Widget(abc.ABC): """Can be used like size hint in QT""" self._parent_rect = parent_rect + @property + def is_pressed(self) -> bool: + return any(self._is_pressed) + + @property + def enabled(self) -> bool: + return self._enabled() if callable(self._enabled) else self._enabled + + def set_enabled(self, enabled: bool | Callable[[], bool]) -> None: + self._enabled = enabled + + @property + def is_visible(self) -> bool: + return self._is_visible() if callable(self._is_visible) else self._is_visible + + def set_visible(self, visible: bool | Callable[[], bool]) -> None: + self._is_visible = visible + + def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: + """Set a callback to determine if the widget can be clicked.""" + self._touch_valid_callback = touch_callback + + def _touch_valid(self) -> bool: + """Check if the widget can be touched.""" + return self._touch_valid_callback() if self._touch_valid_callback else True + def set_position(self, x: float, y: float) -> None: changed = (self._rect.x != x or self._rect.y != y) self._rect.x, self._rect.y = x, y @@ -67,18 +82,37 @@ class Widget(abc.ABC): ret = self._render(self._rect) # Keep track of whether mouse down started within the widget's rectangle - for mouse_event in gui_app.mouse_events: - if mouse_event.left_pressed and self._touch_valid(): - if rl.check_collision_point_rec(mouse_event.pos, self._rect): - self._is_pressed = True + if self.enabled: + for mouse_event in gui_app.mouse_events: + if not self._multi_touch and mouse_event.slot != 0: + continue - elif not self._touch_valid(): - self._is_pressed = False + # Ignores touches/presses that start outside our rect + # Allows touch to leave the rect and come back in focus if mouse did not release + if mouse_event.left_pressed and self._touch_valid(): + if rl.check_collision_point_rec(mouse_event.pos, self._rect): + self._is_pressed[mouse_event.slot] = True + self._tracking_is_pressed[mouse_event.slot] = True - elif mouse_event.left_released: - if self._is_pressed and rl.check_collision_point_rec(mouse_event.pos, self._rect): - self._handle_mouse_release(mouse_event.pos) - self._is_pressed = False + # Callback such as scroll panel signifies user is scrolling + elif not self._touch_valid(): + self._is_pressed[mouse_event.slot] = False + self._tracking_is_pressed[mouse_event.slot] = False + + elif mouse_event.left_released: + if self._is_pressed[mouse_event.slot] and rl.check_collision_point_rec(mouse_event.pos, self._rect): + self._handle_mouse_release(mouse_event.pos) + self._is_pressed[mouse_event.slot] = False + self._tracking_is_pressed[mouse_event.slot] = False + + # Mouse/touch is still within our rect + elif rl.check_collision_point_rec(mouse_event.pos, self._rect): + if self._tracking_is_pressed[mouse_event.slot]: + self._is_pressed[mouse_event.slot] = True + + # Mouse/touch left our rect but may come back into focus later + elif not rl.check_collision_point_rec(mouse_event.pos, self._rect): + self._is_pressed[mouse_event.slot] = False return ret diff --git a/system/ui/widgets/button.py b/system/ui/widgets/button.py index 2be56e6dd..41af8e51c 100644 --- a/system/ui/widgets/button.py +++ b/system/ui/widgets/button.py @@ -6,6 +6,7 @@ import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import TextAlignment, Label class ButtonStyle(IntEnum): @@ -15,17 +16,15 @@ class ButtonStyle(IntEnum): TRANSPARENT = 3 # For buttons with transparent background and border ACTION = 4 LIST_ACTION = 5 # For list items with action buttons - - -class TextAlignment(IntEnum): - LEFT = 0 - CENTER = 1 - RIGHT = 2 + NO_EFFECT = 6 + KEYBOARD = 7 + FORGET_WIFI = 8 ICON_PADDING = 15 DEFAULT_BUTTON_FONT_SIZE = 60 BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51) +BUTTON_DISABLED_BACKGROUND_COLOR = rl.Color(51, 51, 51, 255) ACTION_BUTTON_FONT_SIZE = 48 BUTTON_TEXT_COLOR = { @@ -35,6 +34,9 @@ BUTTON_TEXT_COLOR = { ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.ACTION: rl.Color(0, 0, 0, 255), ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255), + ButtonStyle.NO_EFFECT: rl.Color(228, 228, 228, 255), + ButtonStyle.KEYBOARD: rl.Color(221, 221, 221, 255), + ButtonStyle.FORGET_WIFI: rl.Color(51, 51, 51, 255), } BUTTON_BACKGROUND_COLORS = { @@ -44,6 +46,9 @@ BUTTON_BACKGROUND_COLORS = { ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.ACTION: rl.Color(189, 189, 189, 255), ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255), + ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), + ButtonStyle.KEYBOARD: rl.Color(68, 68, 68, 255), + ButtonStyle.FORGET_WIFI: rl.Color(189, 189, 189, 255), } BUTTON_PRESSED_BACKGROUND_COLORS = { @@ -53,6 +58,9 @@ BUTTON_PRESSED_BACKGROUND_COLORS = { ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.ACTION: rl.Color(130, 130, 130, 255), ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74), + ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), + ButtonStyle.KEYBOARD: rl.Color(51, 51, 51, 255), + ButtonStyle.FORGET_WIFI: rl.Color(130, 130, 130, 255), } _pressed_buttons: set[str] = set() # Track mouse press state globally @@ -162,34 +170,82 @@ class Button(Widget): font_weight: FontWeight = FontWeight.MEDIUM, button_style: ButtonStyle = ButtonStyle.NORMAL, border_radius: int = 10, + text_alignment: TextAlignment = TextAlignment.CENTER, + text_padding: int = 20, + icon = None, + multi_touch: bool = False, ): super().__init__() - self._text = text - self._click_callback = click_callback - self._label_font = gui_app.font(FontWeight.SEMI_BOLD) self._button_style = button_style - self._font_size = font_size self._border_radius = border_radius - self._font_size = font_size - self._text_color = BUTTON_TEXT_COLOR[button_style] - self._text_size = measure_text_cached(gui_app.font(font_weight), text, font_size) + self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style] + + self._label = Label(text, font_size, font_weight, text_alignment, text_padding, + BUTTON_TEXT_COLOR[self._button_style], icon=icon) + + self._click_callback = click_callback + self._multi_touch = multi_touch + + def set_text(self, text): + self._label.set_text(text) def _handle_mouse_release(self, mouse_pos: MousePos): - if self._click_callback: - print(f"Button clicked: {self._text}") + if self._click_callback and self.enabled: self._click_callback() - def _get_background_color(self) -> rl.Color: - if self._is_pressed: - return BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style] - else: - return BUTTON_BACKGROUND_COLORS[self._button_style] + def _update_state(self): + if self.enabled: + self._label.set_text_color(BUTTON_TEXT_COLOR[self._button_style]) + if self.is_pressed: + self._background_color = BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style] + else: + self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style] + elif self._button_style != ButtonStyle.NO_EFFECT: + self._background_color = BUTTON_DISABLED_BACKGROUND_COLOR + self._label.set_text_color(BUTTON_DISABLED_TEXT_COLOR) def _render(self, _): roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2) - rl.draw_rectangle_rounded(self._rect, roundness, 10, self._get_background_color()) + rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color) + self._label.render(self._rect) - text_pos = rl.Vector2(0, self._rect.y + (self._rect.height - self._text_size.y) // 2) - text_pos.x = self._rect.x + (self._rect.width - self._text_size.x) // 2 - rl.draw_text_ex(self._label_font, self._text, text_pos, self._font_size, 0, self._text_color) + +class ButtonRadio(Button): + def __init__(self, + text: str, + icon, + click_callback: Callable[[], None] = None, + font_size: int = DEFAULT_BUTTON_FONT_SIZE, + text_alignment: TextAlignment = TextAlignment.LEFT, + border_radius: int = 10, + text_padding: int = 20, + ): + + super().__init__(text, click_callback=click_callback, font_size=font_size, + border_radius=border_radius, text_padding=text_padding, + text_alignment=text_alignment) + self._text_padding = text_padding + self._icon = icon + self.selected = False + + def _handle_mouse_release(self, mouse_pos: MousePos): + self.selected = not self.selected + if self._click_callback: + self._click_callback() + + def _update_state(self): + if self.selected: + self._background_color = BUTTON_BACKGROUND_COLORS[ButtonStyle.PRIMARY] + else: + self._background_color = BUTTON_BACKGROUND_COLORS[ButtonStyle.NORMAL] + + def _render(self, _): + roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2) + rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color) + self._label.render(self._rect) + + if self._icon and self.selected: + icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 + icon_x = self._rect.x + self._rect.width - self._icon.width - self._text_padding - ICON_PADDING + rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE if self.enabled else rl.Color(255, 255, 255, 100)) diff --git a/system/ui/widgets/confirm_dialog.py b/system/ui/widgets/confirm_dialog.py index 647a455d6..1021b5452 100644 --- a/system/ui/widgets/confirm_dialog.py +++ b/system/ui/widgets/confirm_dialog.py @@ -1,8 +1,9 @@ import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import DialogResult -from openpilot.system.ui.widgets.button import gui_button, ButtonStyle -from openpilot.system.ui.widgets.label import gui_text_box +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle, Button +from openpilot.system.ui.widgets.label import gui_text_box, Label +from openpilot.system.ui.widgets import Widget DIALOG_WIDTH = 1520 DIALOG_HEIGHT = 600 @@ -11,6 +12,59 @@ MARGIN = 50 TEXT_AREA_HEIGHT_REDUCTION = 200 BACKGROUND_COLOR = rl.Color(27, 27, 27, 255) +class ConfirmDialog(Widget): + def __init__(self, text: str, confirm_text: str, cancel_text: str = "Cancel"): + super().__init__() + self._label = Label(text, 70, FontWeight.BOLD) + self._cancel_button = Button(cancel_text, self._cancel_button_callback) + self._confirm_button = Button(confirm_text, self._confirm_button_callback, button_style=ButtonStyle.PRIMARY) + self._dialog_result = DialogResult.NO_ACTION + self._cancel_text = cancel_text + + def set_text(self, text): + self._label.set_text(text) + + def reset(self): + self._dialog_result = DialogResult.NO_ACTION + + def _cancel_button_callback(self): + self._dialog_result = DialogResult.CANCEL + + def _confirm_button_callback(self): + self._dialog_result = DialogResult.CONFIRM + + def _render(self, rect: rl.Rectangle): + dialog_x = (gui_app.width - DIALOG_WIDTH) / 2 + dialog_y = (gui_app.height - DIALOG_HEIGHT) / 2 + dialog_rect = rl.Rectangle(dialog_x, dialog_y, DIALOG_WIDTH, DIALOG_HEIGHT) + + bottom = dialog_rect.y + dialog_rect.height + button_width = (dialog_rect.width - 3 * MARGIN) // 2 + cancel_button_x = dialog_rect.x + MARGIN + confirm_button_x = dialog_rect.x + dialog_rect.width - button_width - MARGIN + button_y = bottom - BUTTON_HEIGHT - MARGIN + cancel_button = rl.Rectangle(cancel_button_x, button_y, button_width, BUTTON_HEIGHT) + confirm_button = rl.Rectangle(confirm_button_x, button_y, button_width, BUTTON_HEIGHT) + + rl.draw_rectangle_rec(dialog_rect, BACKGROUND_COLOR) + + text_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y, dialog_rect.width - 2 * MARGIN, dialog_rect.height - TEXT_AREA_HEIGHT_REDUCTION) + self._label.render(text_rect) + + if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER): + self._dialog_result = DialogResult.CONFIRM + elif rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE): + self._dialog_result = DialogResult.CANCEL + + if self._cancel_text: + self._confirm_button.render(confirm_button) + self._cancel_button.render(cancel_button) + else: + centered_button_x = dialog_rect.x + (dialog_rect.width - button_width) / 2 + centered_confirm_button = rl.Rectangle(centered_button_x, button_y, button_width, BUTTON_HEIGHT) + self._confirm_button.render(centered_confirm_button) + + return self._dialog_result def confirm_dialog(message: str, confirm_text: str, cancel_text: str = "Cancel") -> DialogResult: dialog_x = (gui_app.width - DIALOG_WIDTH) / 2 diff --git a/system/ui/widgets/inputbox.py b/system/ui/widgets/inputbox.py index 65428479b..239d63037 100644 --- a/system/ui/widgets/inputbox.py +++ b/system/ui/widgets/inputbox.py @@ -1,6 +1,6 @@ import pyray as rl import time -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import gui_app, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget @@ -107,9 +107,6 @@ class InputBox(Widget): self._visible_width = rect.width self._font_size = font_size - # Handle mouse input - self._handle_mouse_input(rect, font_size) - # Draw input box rl.draw_rectangle_rec(rect, color) @@ -169,30 +166,27 @@ class InputBox(Widget): return masked_text - def _handle_mouse_input(self, rect, font_size): - """Handle mouse clicks to position cursor.""" - mouse_pos = rl.get_mouse_position() - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and rl.check_collision_point_rec(mouse_pos, rect): - # Calculate cursor position from click - if len(self._input_text) > 0: - font = gui_app.font() - display_text = self._get_display_text() + def _handle_mouse_release(self, mouse_pos: MousePos): + # Calculate cursor position from click + if len(self._input_text) > 0: + font = gui_app.font() + display_text = self._get_display_text() - # Find the closest character position to the click - relative_x = mouse_pos.x - (rect.x + 10) + self._text_offset - best_pos = 0 - min_distance = float('inf') + # Find the closest character position to the click + relative_x = mouse_pos.x - (self._rect.x + 10) + self._text_offset + best_pos = 0 + min_distance = float('inf') - for i in range(len(self._input_text) + 1): - char_width = measure_text_cached(font, display_text[:i], font_size).x - distance = abs(relative_x - char_width) - if distance < min_distance: - min_distance = distance - best_pos = i + for i in range(len(self._input_text) + 1): + char_width = measure_text_cached(font, display_text[:i], self._font_size).x + distance = abs(relative_x - char_width) + if distance < min_distance: + min_distance = distance + best_pos = i - self.set_cursor_position(best_pos) - else: - self.set_cursor_position(0) + self.set_cursor_position(best_pos) + else: + self.set_cursor_position(0) def _handle_keyboard_input(self): # Handle navigation keys diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 432d9b3cf..25843d1ce 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -1,11 +1,14 @@ +from functools import partial import time from typing import Literal + import pyray as rl + from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import ButtonStyle, gui_button +from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.inputbox import InputBox -from openpilot.system.ui.widgets.label import gui_label +from openpilot.system.ui.widgets.label import Label, TextAlignment KEY_FONT_SIZE = 96 DOUBLE_CLICK_THRESHOLD = 0.5 # seconds @@ -59,8 +62,8 @@ class Keyboard(Widget): self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase" self._caps_lock = False self._last_shift_press_time = 0 - self._title = "" - self._sub_title = "" + self._title = Label("", 90, FontWeight.BOLD, TextAlignment.LEFT) + self._sub_title = Label("", 55, FontWeight.NORMAL, TextAlignment.LEFT) self._max_text_size = max_text_size self._min_text_size = min_text_size @@ -73,6 +76,11 @@ class Keyboard(Widget): self._backspace_press_time: float = 0.0 self._backspace_last_repeat: float = 0.0 + self._render_return_status = -1 + self._cancel_button = Button("Cancel", self._cancel_button_callback) + + self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT) + self._eye_open_texture = gui_app.texture("icons/eye_open.png", 81, 54) self._eye_closed_texture = gui_app.texture("icons/eye_closed.png", 81, 54) self._key_icons = { @@ -83,6 +91,19 @@ class Keyboard(Widget): ENTER_KEY: gui_app.texture("icons/arrow-right.png", 80, 80), } + self._all_keys = {} + for l in KEYBOARD_LAYOUTS: + for _, keys in enumerate(KEYBOARD_LAYOUTS[l]): + for _, key in enumerate(keys): + if key in self._key_icons: + texture = self._key_icons[key] + self._all_keys[key] = Button("", partial(self._key_callback, key), icon=texture, + button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD, multi_touch=True) + else: + self._all_keys[key] = Button(key, partial(self._key_callback, key), button_style=ButtonStyle.KEYBOARD, font_size=85, multi_touch=True) + self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY], + button_style=ButtonStyle.KEYBOARD, multi_touch=True) + @property def text(self): return self._input_box.text @@ -94,16 +115,27 @@ class Keyboard(Widget): self._backspace_pressed = False def set_title(self, title: str, sub_title: str = ""): - self._title = title - self._sub_title = sub_title + self._title.set_text(title) + self._sub_title.set_text(sub_title) + + def _eye_button_callback(self): + self._password_mode = not self._password_mode + + def _cancel_button_callback(self): + self.clear() + self._render_return_status = 0 + + def _key_callback(self, k): + if k == ENTER_KEY: + self._render_return_status = 1 + else: + self.handle_key_press(k) def _render(self, rect: rl.Rectangle): rect = rl.Rectangle(rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, rect.height - 2 * CONTENT_MARGIN) - gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), self._title, 90, font_weight=FontWeight.BOLD) - gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), self._sub_title, 55, font_weight=FontWeight.NORMAL) - if gui_button(rl.Rectangle(rect.x + rect.width - 386, rect.y, 386, 125), "Cancel"): - self.clear() - return 0 + self._title.render(rl.Rectangle(rect.x, rect.y, rect.width, 95)) + self._sub_title.render(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60)) + self._cancel_button.render(rl.Rectangle(rect.x + rect.width - 386, rect.y, 386, 125)) # Draw input box and password toggle input_margin = 25 @@ -111,7 +143,7 @@ class Keyboard(Widget): self._render_input_area(input_box_rect) # Process backspace key repeat if it's held down - if not rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT): + if not self._all_keys[BACKSPACE_KEY].is_pressed: self._backspace_pressed = False if self._backspace_pressed: @@ -146,33 +178,22 @@ class Keyboard(Widget): start_x += new_width is_enabled = key != ENTER_KEY or len(self._input_box.text) >= self._min_text_size - result = -1 - # Check for backspace key press-and-hold - mouse_pos = rl.get_mouse_position() - mouse_over_key = rl.check_collision_point_rec(mouse_pos, key_rect) - - if key == BACKSPACE_KEY and mouse_over_key: - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): - self._backspace_pressed = True - self._backspace_press_time = time.monotonic() - self._backspace_last_repeat = time.monotonic() + if key == BACKSPACE_KEY and self._all_keys[BACKSPACE_KEY].is_pressed and not self._backspace_pressed: + self._backspace_pressed = True + self._backspace_press_time = time.monotonic() + self._backspace_last_repeat = time.monotonic() if key in self._key_icons: if key == SHIFT_ACTIVE_KEY and self._caps_lock: key = CAPS_LOCK_KEY - texture = self._key_icons[key] - result = gui_button(key_rect, "", icon=texture, button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.NORMAL, is_enabled=is_enabled) + self._all_keys[key].set_enabled(is_enabled) + self._all_keys[key].render(key_rect) else: - result = gui_button(key_rect, key, KEY_FONT_SIZE, is_enabled=is_enabled) + self._all_keys[key].set_enabled(is_enabled) + self._all_keys[key].render(key_rect) - if result: - if key == ENTER_KEY: - return 1 - else: - self.handle_key_press(key) - - return -1 + return self._render_return_status def _render_input_area(self, input_rect: rl.Rectangle): if self._show_password_toggle: @@ -183,16 +204,12 @@ class Keyboard(Widget): eye_texture = self._eye_closed_texture if self._password_mode else self._eye_open_texture eye_rect = rl.Rectangle(input_rect.x + input_rect.width - 90, input_rect.y, 80, input_rect.height) + self._eye_button.render(eye_rect) + eye_x = eye_rect.x + (eye_rect.width - eye_texture.width) / 2 eye_y = eye_rect.y + (eye_rect.height - eye_texture.height) / 2 rl.draw_texture_v(eye_texture, rl.Vector2(eye_x, eye_y), rl.WHITE) - - # Handle click on eye icon - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and rl.check_collision_point_rec( - rl.get_mouse_position(), eye_rect - ): - self._password_mode = not self._password_mode else: self._input_box.render(input_rect) @@ -226,6 +243,10 @@ class Keyboard(Widget): if not self._caps_lock and self._layout_name == "uppercase": self._layout_name = "lowercase" + def reset(self): + self._render_return_status = -1 + self.clear() + if __name__ == "__main__": gui_app.init_window("Keyboard") diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index e840e8586..2da9a9f8d 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -1,11 +1,23 @@ +from enum import IntEnum +from itertools import zip_longest + import pyray as rl + from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.utils import GuiStyleContext +from openpilot.system.ui.lib.emoji import find_emoji, emoji_tex +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget +ICON_PADDING = 15 + +class TextAlignment(IntEnum): + LEFT = 0 + CENTER = 1 + RIGHT = 2 # TODO: This should be a Widget class - def gui_label( rect: rl.Rectangle, text: str, @@ -78,3 +90,88 @@ def gui_text_box( if font_weight != FontWeight.NORMAL: rl.gui_set_font(gui_app.font(FontWeight.NORMAL)) + + +# Non-interactive text area. Can render emojis and an optional specified icon. +class Label(Widget): + def __init__(self, + text: str, + font_size: int = DEFAULT_TEXT_SIZE, + font_weight: FontWeight = FontWeight.NORMAL, + text_alignment: TextAlignment = TextAlignment.CENTER, + text_padding: int = 20, + text_color: rl.Color = DEFAULT_TEXT_COLOR, + icon = None, + ): + + super().__init__() + self._font_weight = font_weight + self._font = gui_app.font(self._font_weight) + self._font_size = font_size + self._text_alignment = text_alignment + self._text_padding = text_padding + self._text_color = text_color + self._icon = icon + self.set_text(text) + + def set_text(self, text): + self._text_raw = text + self._update_text(self._text_raw) + + def set_text_color(self, color): + self._text_color = color + + def _update_layout_rects(self): + self._update_text(self._text_raw) + + def _update_text(self, text): + self._emojis = [] + self._text_size = [] + self._text = wrap_text(self._font, text, self._font_size, self._rect.width - (self._text_padding*2)) + for t in self._text: + self._emojis.append(find_emoji(t)) + self._text_size.append(measure_text_cached(self._font, t, self._font_size)) + + def _render(self, _): + text = self._text[0] if self._text else None + text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0) + text_pos = rl.Vector2(0, (self._rect.y + (self._rect.height - (text_size.y)) // 2)) + + if self._icon: + icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 + if text: + if self._text_alignment == TextAlignment.LEFT: + icon_x = self._rect.x + self._text_padding + text_pos.x = self._icon.width + ICON_PADDING + elif self._text_alignment == TextAlignment.CENTER: + total_width = self._icon.width + ICON_PADDING + text_size.x + icon_x = self._rect.x + (self._rect.width - total_width) / 2 + text_pos.x = self._icon.width + ICON_PADDING + else: + icon_x = (self._rect.x + self._rect.width - text_size.x - self._text_padding) - ICON_PADDING - self._icon.width + else: + icon_x = self._rect.x + (self._rect.width - self._icon.width) / 2 + rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE) + + for text, text_size, emojis in zip_longest(self._text, self._text_size, self._emojis, fillvalue=[]): + line_pos = rl.Vector2(text_pos.x, text_pos.y) + if self._text_alignment == TextAlignment.LEFT: + line_pos.x += self._rect.x + self._text_padding + elif self._text_alignment == TextAlignment.CENTER: + line_pos.x += self._rect.x + (self._rect.width - text_size.x) // 2 + elif self._text_alignment == TextAlignment.RIGHT: + line_pos.x += self._rect.x + self._rect.width - text_size.x - self._text_padding + + prev_index = 0 + for start, end, emoji in emojis: + text_before = text[prev_index:start] + width_before = measure_text_cached(self._font, text_before, self._font_size) + rl.draw_text_ex(self._font, text_before, line_pos, self._font_size, 0, self._text_color) + line_pos.x += width_before.x + + tex = emoji_tex(emoji) + rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height, self._text_color) + line_pos.x += self._font_size + prev_index = end + rl.draw_text_ex(self._font, text[prev_index:], line_pos, self._font_size, 0, self._text_color) + text_pos.y += text_size.y or self._font_size diff --git a/system/ui/widgets/list_view.py b/system/ui/widgets/list_view.py index aa2fdb845..e871eef0a 100644 --- a/system/ui/widgets/list_view.py +++ b/system/ui/widgets/list_view.py @@ -167,7 +167,7 @@ class MultipleButtonAction(ItemAction): # Check button state mouse_pos = rl.get_mouse_position() is_hovered = rl.check_collision_point_rec(mouse_pos, button_rect) - is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed + is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed is_selected = i == self.selected_button # Button colors @@ -188,7 +188,7 @@ class MultipleButtonAction(ItemAction): rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, rl.Color(228, 228, 228, 255)) # Handle click - if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed: + if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed: clicked = i if clicked >= 0: diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 63e26f0cc..4eac1214c 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from functools import partial from threading import Lock from typing import Literal @@ -7,8 +8,8 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.wifi_manager import NetworkInfo, WifiManagerCallbacks, WifiManagerWrapper, SecurityType from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import ButtonStyle, gui_button -from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog +from openpilot.system.ui.widgets.button import ButtonStyle, Button, TextAlignment +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.keyboard import Keyboard from openpilot.system.ui.widgets.label import gui_label @@ -40,6 +41,7 @@ class StateConnecting: @dataclass class StateNeedsAuth: network: NetworkInfo + retry: bool action: Literal["needs_auth"] = "needs_auth" @@ -67,8 +69,11 @@ class WifiManagerUI(Widget): self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True) self._networks: list[NetworkInfo] = [] + self._networks_buttons: dict[str, Button] = {} + self._forget_networks_buttons: dict[str, Button] = {} self._lock = Lock() self.wifi_manager = wifi_manager + self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel") self.wifi_manager.set_callbacks( WifiManagerCallbacks( @@ -89,12 +94,14 @@ class WifiManagerUI(Widget): return match self.state: - case StateNeedsAuth(network): - self.keyboard.set_title("Enter password", f"for {network.ssid}") + case StateNeedsAuth(network, retry): + self.keyboard.set_title("Wrong password" if retry else "Enter password", f"for {network.ssid}") + self.keyboard.reset() gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(network, result)) case StateShowForgetConfirm(network): - gui_app.set_modal_overlay(lambda: confirm_dialog(f'Forget Wi-Fi Network "{network.ssid}"?', "Forget"), - callback=lambda result: self.on_forgot_confirm_finished(network, result)) + self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{network.ssid}"?') + self._confirm_dialog.reset() + gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(network, result)) case _: self._draw_network_list(rect) @@ -139,16 +146,20 @@ class WifiManagerUI(Widget): signal_icon_rect = rl.Rectangle(rect.x + rect.width - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE) security_icon_rect = rl.Rectangle(signal_icon_rect.x - spacing - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE) - gui_label(ssid_rect, network.ssid, 55) - status_text = "" match self.state: case StateConnecting(network=connecting): if connecting.ssid == network.ssid: + self._networks_buttons[network.ssid].set_enabled(False) status_text = "CONNECTING..." case StateForgetting(network=forgetting): if forgetting.ssid == network.ssid: + self._networks_buttons[network.ssid].set_enabled(False) status_text = "FORGETTING..." + case _: + self._networks_buttons[network.ssid].set_enabled(True) + + self._networks_buttons[network.ssid].render(ssid_rect) if status_text: status_text_rect = rl.Rectangle(security_icon_rect.x - 410, rect.y, 410, ITEM_HEIGHT) @@ -162,18 +173,22 @@ class WifiManagerUI(Widget): self.btn_width, 80, ) - if isinstance(self.state, StateIdle) and gui_button(forget_btn_rect, "Forget", button_style=ButtonStyle.ACTION) and clicked: - self.state = StateShowForgetConfirm(network) + self._forget_networks_buttons[network.ssid].render(forget_btn_rect) self._draw_status_icon(security_icon_rect, network) self._draw_signal_strength_icon(signal_icon_rect, network) - if isinstance(self.state, StateIdle) and rl.check_collision_point_rec(rl.get_mouse_position(), ssid_rect) and clicked: + def _networks_buttons_callback(self, network): + if self.scroll_panel.is_touch_valid(): if not network.is_saved and network.security_type != SecurityType.OPEN: - self.state = StateNeedsAuth(network) + self.state = StateNeedsAuth(network, False) elif not network.is_connected: self.connect_to_network(network) + def _forget_networks_buttons_callback(self, network): + if self.scroll_panel.is_touch_valid(): + self.state = StateShowForgetConfirm(network) + def _draw_status_icon(self, rect, network: NetworkInfo): """Draw the status icon based on network's connection state""" icon_file = None @@ -211,12 +226,17 @@ class WifiManagerUI(Widget): def _on_network_updated(self, networks: list[NetworkInfo]): with self._lock: self._networks = networks + for n in self._networks: + self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, text_alignment=TextAlignment.LEFT, + button_style=ButtonStyle.NO_EFFECT) + self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, + font_size=45) def _on_need_auth(self, ssid): with self._lock: network = next((n for n in self._networks if n.ssid == ssid), None) if network: - self.state = StateNeedsAuth(network) + self.state = StateNeedsAuth(network, True) def _on_activated(self): with self._lock: diff --git a/system/ui/widgets/toggle.py b/system/ui/widgets/toggle.py index 3f7f46391..eba5ef9e5 100644 --- a/system/ui/widgets/toggle.py +++ b/system/ui/widgets/toggle.py @@ -38,9 +38,6 @@ class Toggle(Widget): self._state = state self._target = 1.0 if state else 0.0 - def set_enabled(self, enabled: bool): - self._enabled = enabled - def is_enabled(self): return self._enabled diff --git a/third_party/raylib/build.sh b/third_party/raylib/build.sh index 9e85f90df..544feb1c5 100755 --- a/third_party/raylib/build.sh +++ b/third_party/raylib/build.sh @@ -30,7 +30,7 @@ fi cd raylib_repo -COMMIT=${1:-66030a7de62c9e1ee8ab30a1d657a740333bb4f2} +COMMIT=${1:-39e6d8b52db159ba2ab3214b46d89a8069e09394} git fetch origin $COMMIT git reset --hard $COMMIT git clean -xdff . diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 91edfafe0..89be3cceb 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -15,7 +15,8 @@ else: qt_libs = ['qt_util'] + base_libs cabana_env = qt_env.Clone() -cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'panda', 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs + +cabana_libs = [widgets, cereal, messaging, visionipc, replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'usb-1.0'] + qt_libs opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../opendbc/dbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] @@ -29,7 +30,7 @@ cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcans 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', - 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', + 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) cabana_env.Program('cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) @@ -40,4 +41,4 @@ output_json_file = 'tools/cabana/dbc/car_fingerprint_to_dbc.json' generate_dbc = cabana_env.Command('#' + output_json_file, ['dbc/generate_dbc_json.py'], "python3 tools/cabana/dbc/generate_dbc_json.py --out " + output_json_file) -cabana_env.Depends(generate_dbc, ["#common", "#selfdrive/pandad", '#opendbc_repo', "#cereal", "#msgq_repo"]) +cabana_env.Depends(generate_dbc, ["#common", '#opendbc_repo', "#cereal", "#msgq_repo"]) diff --git a/tools/cabana/panda.cc b/tools/cabana/panda.cc new file mode 100644 index 000000000..0612d6774 --- /dev/null +++ b/tools/cabana/panda.cc @@ -0,0 +1,346 @@ +#include "tools/cabana/panda.h" + +#include +#include +#include +#include +#include + +#include "cereal/messaging/messaging.h" +#include "common/swaglog.h" +#include "common/util.h" + +static libusb_context *init_usb_ctx() { + libusb_context *context = nullptr; + int err = libusb_init(&context); + if (err != 0) { + LOGE("libusb initialization error"); + return nullptr; + } + +#if LIBUSB_API_VERSION >= 0x01000106 + libusb_set_option(context, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_INFO); +#else + libusb_set_debug(context, 3); +#endif + return context; +} + +Panda::Panda(std::string serial, uint32_t bus_offset) : bus_offset(bus_offset) { + if (!init_usb_connection(serial)) { + throw std::runtime_error("Error connecting to panda"); + } + + LOGW("connected to %s over USB", serial.c_str()); + hw_type = get_hw_type(); + can_reset_communications(); +} + +Panda::~Panda() { + cleanup_usb(); +} + +bool Panda::connected() { + return connected_flag; +} + +bool Panda::comms_healthy() { + return comms_healthy_flag; +} + +std::string Panda::hw_serial() { + return hw_serial_str; +} + +std::vector Panda::list(bool usb_only) { + static std::unique_ptr context(init_usb_ctx(), libusb_exit); + + ssize_t num_devices; + libusb_device **dev_list = NULL; + std::vector serials; + if (!context) { return serials; } + + num_devices = libusb_get_device_list(context.get(), &dev_list); + if (num_devices < 0) { + LOGE("libusb can't get device list"); + goto finish; + } + + for (size_t i = 0; i < num_devices; ++i) { + libusb_device *device = dev_list[i]; + libusb_device_descriptor desc; + libusb_get_device_descriptor(device, &desc); + if (desc.idVendor == 0x3801 && desc.idProduct == 0xddcc) { + libusb_device_handle *handle = NULL; + int ret = libusb_open(device, &handle); + if (ret < 0) { goto finish; } + + unsigned char desc_serial[26] = { 0 }; + ret = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); + libusb_close(handle); + if (ret < 0) { goto finish; } + + serials.push_back(std::string((char *)desc_serial, ret)); + } + } + +finish: + if (dev_list != NULL) { + libusb_free_device_list(dev_list, 1); + } + return serials; +} + +void Panda::set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param) { + control_write(0xdc, (uint16_t)safety_model, safety_param); +} + + +cereal::PandaState::PandaType Panda::get_hw_type() { + unsigned char hw_query[1] = {0}; + + control_read(0xc1, 0, 0, hw_query, 1); + return (cereal::PandaState::PandaType)(hw_query[0]); +} + + + + +void Panda::send_heartbeat(bool engaged) { + control_write(0xf3, engaged, 0); +} + +void Panda::set_can_speed_kbps(uint16_t bus, uint16_t speed) { + control_write(0xde, bus, (speed * 10)); +} + + +void Panda::set_data_speed_kbps(uint16_t bus, uint16_t speed) { + control_write(0xf9, bus, (speed * 10)); +} + + + +bool Panda::can_receive(std::vector& out_vec) { + // Check if enough space left in buffer to store RECV_SIZE data + assert(receive_buffer_size + RECV_SIZE <= sizeof(receive_buffer)); + + int recv = bulk_read(0x81, &receive_buffer[receive_buffer_size], RECV_SIZE); + if (!comms_healthy()) { + return false; + } + + bool ret = true; + if (recv > 0) { + receive_buffer_size += recv; + ret = unpack_can_buffer(receive_buffer, receive_buffer_size, out_vec); + } + return ret; +} + +void Panda::can_reset_communications() { + control_write(0xc0, 0, 0); +} + +bool Panda::unpack_can_buffer(uint8_t *data, uint32_t &size, std::vector &out_vec) { + int pos = 0; + + while (pos <= size - sizeof(can_header)) { + can_header header; + memcpy(&header, &data[pos], sizeof(can_header)); + + const uint8_t data_len = dlc_to_len[header.data_len_code]; + if (pos + sizeof(can_header) + data_len > size) { + // we don't have all the data for this message yet + break; + } + + if (calculate_checksum(&data[pos], sizeof(can_header) + data_len) != 0) { + LOGE("Panda CAN checksum failed"); + size = 0; + can_reset_communications(); + return false; + } + + can_frame &canData = out_vec.emplace_back(); + canData.address = header.addr; + canData.src = header.bus + bus_offset; + if (header.rejected) { + canData.src += CAN_REJECTED_BUS_OFFSET; + } + if (header.returned) { + canData.src += CAN_RETURNED_BUS_OFFSET; + } + + canData.dat.assign((char *)&data[pos + sizeof(can_header)], data_len); + + pos += sizeof(can_header) + data_len; + } + + // move the overflowing data to the beginning of the buffer for the next round + memmove(data, &data[pos], size - pos); + size -= pos; + + return true; +} + +uint8_t Panda::calculate_checksum(uint8_t *data, uint32_t len) { + uint8_t checksum = 0U; + for (uint32_t i = 0U; i < len; i++) { + checksum ^= data[i]; + } + return checksum; +} + +// USB implementation methods +bool Panda::init_usb_connection(const std::string& serial) { + ssize_t num_devices; + libusb_device **dev_list = NULL; + int err = 0; + + ctx = init_usb_ctx(); + if (!ctx) { goto fail; } + + // connect by serial + num_devices = libusb_get_device_list(ctx, &dev_list); + if (num_devices < 0) { goto fail; } + + for (size_t i = 0; i < num_devices; ++i) { + libusb_device_descriptor desc; + libusb_get_device_descriptor(dev_list[i], &desc); + if (desc.idVendor == 0x3801 && desc.idProduct == 0xddcc) { + int ret = libusb_open(dev_list[i], &dev_handle); + if (dev_handle == NULL || ret < 0) { goto fail; } + + unsigned char desc_serial[26] = { 0 }; + ret = libusb_get_string_descriptor_ascii(dev_handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); + if (ret < 0) { goto fail; } + + hw_serial_str = std::string((char *)desc_serial, ret); + if (serial.empty() || serial == hw_serial_str) { + break; + } + libusb_close(dev_handle); + dev_handle = NULL; + } + } + if (dev_handle == NULL) goto fail; + libusb_free_device_list(dev_list, 1); + + if (libusb_kernel_driver_active(dev_handle, 0) == 1) { + libusb_detach_kernel_driver(dev_handle, 0); + } + + err = libusb_set_configuration(dev_handle, 1); + if (err != 0) { goto fail; } + + err = libusb_claim_interface(dev_handle, 0); + if (err != 0) { goto fail; } + + return true; + +fail: + if (dev_list != NULL) { + libusb_free_device_list(dev_list, 1); + } + cleanup_usb(); + return false; +} + +void Panda::cleanup_usb() { + if (dev_handle) { + libusb_release_interface(dev_handle, 0); + libusb_close(dev_handle); + dev_handle = nullptr; + } + + if (ctx) { + libusb_exit(ctx); + ctx = nullptr; + } + connected_flag = false; +} + +void Panda::handle_usb_issue(int err, const char func[]) { + LOGE_100("usb error %d \"%s\" in %s", err, libusb_strerror((enum libusb_error)err), func); + if (err == LIBUSB_ERROR_NO_DEVICE) { + LOGE("lost connection"); + connected_flag = false; + } +} + +int Panda::control_write(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned int timeout) { + int err; + const uint8_t bmRequestType = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; + + if (!connected_flag) { + return LIBUSB_ERROR_NO_DEVICE; + } + + do { + err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, NULL, 0, timeout); + if (err < 0) handle_usb_issue(err, __func__); + } while (err < 0 && connected_flag); + + return err; +} + +int Panda::control_read(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned char *data, uint16_t wLength, unsigned int timeout) { + int err; + const uint8_t bmRequestType = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; + + if (!connected_flag) { + return LIBUSB_ERROR_NO_DEVICE; + } + + do { + err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, data, wLength, timeout); + if (err < 0) handle_usb_issue(err, __func__); + } while (err < 0 && connected_flag); + + return err; +} + +int Panda::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int err; + int transferred = 0; + + if (!connected_flag) { + return 0; + } + + do { + err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); + if (err == LIBUSB_ERROR_TIMEOUT) { + LOGW("Transmit buffer full"); + break; + } else if (err != 0 || length != transferred) { + handle_usb_issue(err, __func__); + } + } while (err != 0 && connected_flag); + + return transferred; +} + +int Panda::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int err; + int transferred = 0; + + if (!connected_flag) { + return 0; + } + + do { + err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); + if (err == LIBUSB_ERROR_TIMEOUT) { + break; // timeout is okay to exit, recv still happened + } else if (err == LIBUSB_ERROR_OVERFLOW) { + comms_healthy_flag = false; + LOGE_100("overflow got 0x%x", transferred); + } else if (err != 0) { + handle_usb_issue(err, __func__); + } + } while (err != 0 && connected_flag); + + return transferred; +} diff --git a/tools/cabana/panda.h b/tools/cabana/panda.h new file mode 100644 index 000000000..d318c33f4 --- /dev/null +++ b/tools/cabana/panda.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "cereal/gen/cpp/car.capnp.h" +#include "cereal/gen/cpp/log.capnp.h" +#include "panda/board/health.h" +#include "panda/board/can.h" + +#define USB_TX_SOFT_LIMIT (0x100U) +#define USBPACKET_MAX_SIZE (0x40) +#define RECV_SIZE (0x4000U) +#define TIMEOUT 0 + +#define CAN_REJECTED_BUS_OFFSET 0xC0U +#define CAN_RETURNED_BUS_OFFSET 0x80U + +#define PANDA_BUS_OFFSET 4 + +struct __attribute__((packed)) can_header { + uint8_t reserved : 1; + uint8_t bus : 3; + uint8_t data_len_code : 4; + uint8_t rejected : 1; + uint8_t returned : 1; + uint8_t extended : 1; + uint32_t addr : 29; + uint8_t checksum : 8; +}; + +struct can_frame { + long address; + std::string dat; + long src; +}; + + +class Panda { +public: + Panda(std::string serial="", uint32_t bus_offset=0); + ~Panda(); + + cereal::PandaState::PandaType hw_type = cereal::PandaState::PandaType::UNKNOWN; + const uint32_t bus_offset; + + bool connected(); + bool comms_healthy(); + std::string hw_serial(); + + // Static functions + static std::vector list(bool usb_only=false); + + // Panda functionality + cereal::PandaState::PandaType get_hw_type(); + void set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param=0U); + void send_heartbeat(bool engaged); + void set_can_speed_kbps(uint16_t bus, uint16_t speed); + void set_data_speed_kbps(uint16_t bus, uint16_t speed); + bool can_receive(std::vector& out_vec); + void can_reset_communications(); + +private: + // USB connection members + libusb_context *ctx = nullptr; + libusb_device_handle *dev_handle = nullptr; + std::string hw_serial_str; + std::atomic connected_flag = true; + std::atomic comms_healthy_flag = true; + + // CAN buffer members + uint8_t receive_buffer[RECV_SIZE + sizeof(can_header) + 64]; + uint32_t receive_buffer_size = 0; + + // Internal methods + bool init_usb_connection(const std::string& serial); + void cleanup_usb(); + void handle_usb_issue(int err, const char func[]); + int control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout=TIMEOUT); + int control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout=TIMEOUT); + int bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + bool unpack_can_buffer(uint8_t *data, uint32_t &size, std::vector &out_vec); + uint8_t calculate_checksum(uint8_t *data, uint32_t len); +}; diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc index e66d72e59..a2430c665 100644 --- a/tools/cabana/streams/pandastream.cc +++ b/tools/cabana/streams/pandastream.cc @@ -24,7 +24,7 @@ bool PandaStream::connect() { return false; } - panda->set_safety_model(cereal::CarParams::SafetyModel::SILENT); + panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT); for (int bus = 0; bus < config.bus_config.size(); bus++) { panda->set_can_speed_kbps(bus, config.bus_config[bus].can_speed_kbps); @@ -72,7 +72,7 @@ void PandaStream::streamThread() { handleEvent(capnp::messageToFlatArray(msg)); - panda->send_heartbeat(false, false); + panda->send_heartbeat(false); } } diff --git a/tools/cabana/streams/pandastream.h b/tools/cabana/streams/pandastream.h index 826b1aa98..e17ad887f 100644 --- a/tools/cabana/streams/pandastream.h +++ b/tools/cabana/streams/pandastream.h @@ -7,7 +7,7 @@ #include #include "tools/cabana/streams/livestream.h" -#include "selfdrive/pandad/panda.h" +#include "tools/cabana/panda.h" const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U}; const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U}; diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index b817ae120..c857c9ffb 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -19,7 +19,7 @@ const int THUMBNAIL_MARGIN = 3; static const QColor timeline_colors[] = { [(int)TimelineType::None] = QColor(111, 143, 175), [(int)TimelineType::Engaged] = QColor(0, 163, 108), - [(int)TimelineType::UserFlag] = Qt::magenta, + [(int)TimelineType::UserBookmark] = Qt::magenta, [(int)TimelineType::AlertInfo] = Qt::green, [(int)TimelineType::AlertWarning] = QColor(255, 195, 0), [(int)TimelineType::AlertCritical] = QColor(199, 0, 57), @@ -64,7 +64,7 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { Pause/Resume:  space  )").arg(timeline_colors[(int)TimelineType::None].name(), timeline_colors[(int)TimelineType::Engaged].name(), - timeline_colors[(int)TimelineType::UserFlag].name(), + timeline_colors[(int)TimelineType::UserBookmark].name(), timeline_colors[(int)TimelineType::AlertInfo].name(), timeline_colors[(int)TimelineType::AlertWarning].name(), timeline_colors[(int)TimelineType::AlertCritical].name())); diff --git a/tools/clip/run.py b/tools/clip/run.py index a84290f9c..792075144 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -29,7 +29,7 @@ FRAMERATE = 20 PIXEL_DEPTH = '24' RESOLUTION = '2160x1080' SECONDS_TO_WARM = 2 -PROC_WAIT_SECONDS = 30 +PROC_WAIT_SECONDS = 30*10 OPENPILOT_FONT = str(Path(BASEDIR, 'selfdrive/assets/fonts/Inter-Regular.ttf').resolve()) REPLAY = str(Path(BASEDIR, 'tools/replay/replay').resolve()) @@ -130,7 +130,7 @@ def populate_car_params(lr: LogReader): for cp in entries: key, value = cp.key, cp.value try: - params.put(key, value) + params.put(key, params.cpp2python(key, value)) except UnknownKeyName: # forks of openpilot may have other Params keys configured. ignore these logger.warning(f"unknown Params key '{key}', skipping") @@ -188,6 +188,7 @@ def clip( out: str, start: int, end: int, + speed: int, target_mb: int, title: str | None, ): @@ -212,6 +213,12 @@ def clip( if title: overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=32:{box_style}:x=(w-text_w)/2:y=53") + if speed > 1: + overlays += [ + f"setpts=PTS/{speed}", + "fps=60", + ] + ffmpeg_cmd = [ 'ffmpeg', '-y', '-video_size', RESOLUTION, @@ -289,6 +296,7 @@ def main(): p.add_argument('-o', '--output', help='output clip to (.mp4)', type=validate_output_file, default=DEFAULT_OUTPUT) p.add_argument('-p', '--prefix', help='openpilot prefix', default=f'clip_{randint(100, 99999)}') p.add_argument('-q', '--quality', help='quality of camera (low = qcam, high = hevc)', choices=['low', 'high'], default='high') + p.add_argument('-x', '--speed', help='record the clip at this speed multiple', type=int, default=1) p.add_argument('-s', '--start', help='start clipping at seconds', type=int) p.add_argument('-t', '--title', help='overlay this title on the video (e.g. "Chill driving across the Golden Gate Bridge")', type=validate_title) args = parse_args(p) @@ -302,6 +310,7 @@ def main(): out=args.output, start=args.start, end=args.end, + speed=args.speed, target_mb=args.file_size, title=args.title, ) diff --git a/tools/lib/logreader.py b/tools/lib/logreader.py index 1075bd2f0..cc84c8b52 100755 --- a/tools/lib/logreader.py +++ b/tools/lib/logreader.py @@ -21,8 +21,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.tools.lib.comma_car_segments import get_url as get_comma_segments_url from openpilot.tools.lib.openpilotci import get_url from openpilot.tools.lib.filereader import DATA_ENDPOINT, FileReader, file_exists, internal_source_available -from openpilot.tools.lib.route import QCAMERA_FILENAMES, CAMERA_FILENAMES, DCAMERA_FILENAMES, \ - ECAMERA_FILENAMES, BOOTLOG_FILENAMES, Route, SegmentRange +from openpilot.tools.lib.route import Route, SegmentRange, FileName from openpilot.tools.lib.log_time_series import msgs_to_time_series LogMessage = type[capnp._DynamicStructReader] @@ -50,8 +49,46 @@ def decompress_stream(data: bytes): return decompressed_data + +class CachedEventReader: + __slots__ = ('_evt', '_enum') + + def __init__(self, evt: capnp._DynamicStructReader, _enum: str | None = None): + """All capnp attribute accesses are expensive, and which() is often called multiple times""" + self._evt = evt + self._enum: str | None = _enum + + # fast pickle support + def __reduce__(self): + return CachedEventReader._reducer, (self._evt.as_builder().to_bytes(), self._enum) + + @staticmethod + def _reducer(data: bytes, _enum: str | None = None): + with capnp_log.Event.from_bytes(data) as evt: + return CachedEventReader(evt, _enum) + + def __repr__(self): + return self._evt.__repr__() + + def __str__(self): + return self._evt.__str__() + + def __dir__(self): + return dir(self._evt) + + def which(self) -> str: + if self._enum is None: + self._enum = self._evt.which() + return self._enum + + def __getattr__(self, name: str): + if name.startswith("__") and name.endswith("__"): + return getattr(self, name) + return getattr(self._evt, name) + + class _LogFileReader: - def __init__(self, fn, canonicalize=True, only_union_types=False, sort_by_time=False, dat=None): + def __init__(self, fn, only_union_types=False, sort_by_time=False, dat=None): self.data_version = None self._only_union_types = only_union_types @@ -76,7 +113,7 @@ class _LogFileReader: self._ents = [] try: for e in ents: - self._ents.append(e) + self._ents.append(CachedEventReader(e)) except capnp.KjException: warnings.warn("Corrupted events detected", RuntimeWarning, stacklevel=1) @@ -102,19 +139,9 @@ class ReadMode(enum.StrEnum): AUTO_INTERACTIVE = "i" # default to rlogs, fallback to qlogs with a prompt from the user -class FileName(enum.Enum): - #TODO use the ones from route.py - RLOG = ("rlog.zst", "rlog.bz2") - QLOG = ("qlog.zst", "qlog.bz2") - QCAMERA = QCAMERA_FILENAMES - FCAMERA = CAMERA_FILENAMES - ECAMERA = ECAMERA_FILENAMES - DCAMERA = DCAMERA_FILENAMES - BOOTLOG = BOOTLOG_FILENAMES - - LogPath = str | None -Source = Callable[[SegmentRange, FileName], list[LogPath]] +LogFileName = tuple[str, ...] +Source = Callable[[SegmentRange, LogFileName], list[LogPath]] InternalUnavailableException = Exception("Internal source not available") @@ -123,7 +150,7 @@ class LogsUnavailable(Exception): pass -def comma_api_source(sr: SegmentRange, fns: FileName) -> list[LogPath]: +def comma_api_source(sr: SegmentRange, fns: LogFileName) -> list[LogPath]: route = Route(sr.route_name) # comma api will have already checked if the file exists @@ -133,21 +160,21 @@ def comma_api_source(sr: SegmentRange, fns: FileName) -> list[LogPath]: return [route.qlog_paths()[seg] for seg in sr.seg_idxs] -def internal_source(sr: SegmentRange, fns: FileName, endpoint_url: str = DATA_ENDPOINT) -> list[LogPath]: +def internal_source(sr: SegmentRange, fns: LogFileName, endpoint_url: str = DATA_ENDPOINT) -> list[LogPath]: if not internal_source_available(endpoint_url): raise InternalUnavailableException def get_internal_url(sr: SegmentRange, seg, file): return f"{endpoint_url.rstrip('/')}/{sr.dongle_id}/{sr.log_id}/{seg}/{file}" - return eval_source([[get_internal_url(sr, seg, fn) for fn in fns.value] for seg in sr.seg_idxs]) + return eval_source([[get_internal_url(sr, seg, fn) for fn in fns] for seg in sr.seg_idxs]) -def openpilotci_source(sr: SegmentRange, fns: FileName) -> list[LogPath]: - return eval_source([[get_url(sr.route_name, seg, fn) for fn in fns.value] for seg in sr.seg_idxs]) +def openpilotci_source(sr: SegmentRange, fns: LogFileName) -> list[LogPath]: + return eval_source([[get_url(sr.route_name, seg, fn) for fn in fns] for seg in sr.seg_idxs]) -def comma_car_segments_source(sr: SegmentRange, fns: FileName) -> list[LogPath]: +def comma_car_segments_source(sr: SegmentRange, fns: LogFileName) -> list[LogPath]: return eval_source([get_comma_segments_url(sr.route_name, seg) for seg in sr.seg_idxs]) diff --git a/tools/lib/route.py b/tools/lib/route.py index 55ea956c5..882585a15 100644 --- a/tools/lib/route.py +++ b/tools/lib/route.py @@ -10,13 +10,15 @@ from openpilot.tools.lib.auth_config import get_token from openpilot.tools.lib.api import APIError, CommaApi from openpilot.tools.lib.helpers import RE -QLOG_FILENAMES = ('qlog.bz2', 'qlog.zst', 'qlog') -QCAMERA_FILENAMES = ('qcamera.ts',) -LOG_FILENAMES = ('rlog.bz2', 'raw_log.bz2', 'rlog.zst', 'rlog') -CAMERA_FILENAMES = ('fcamera.hevc', 'video.hevc') -DCAMERA_FILENAMES = ('dcamera.hevc',) -ECAMERA_FILENAMES = ('ecamera.hevc',) -BOOTLOG_FILENAMES = ('bootlog.zst', 'bootlog.bz2', 'bootlog') + +class FileName: + RLOG = ("rlog.zst", "rlog.bz2") + QLOG = ("qlog.zst", "qlog.bz2") + QCAMERA = ('qcamera.ts',) + FCAMERA = ('fcamera.hevc',) + ECAMERA = ('ecamera.hevc',) + DCAMERA = ('dcamera.hevc',) + BOOTLOG = ('bootlog.zst', 'bootlog.bz2') class Route: @@ -82,23 +84,23 @@ class Route: if segments.get(segment_name): segments[segment_name] = Segment( segment_name, - url if fn in LOG_FILENAMES else segments[segment_name].log_path, - url if fn in QLOG_FILENAMES else segments[segment_name].qlog_path, - url if fn in CAMERA_FILENAMES else segments[segment_name].camera_path, - url if fn in DCAMERA_FILENAMES else segments[segment_name].dcamera_path, - url if fn in ECAMERA_FILENAMES else segments[segment_name].ecamera_path, - url if fn in QCAMERA_FILENAMES else segments[segment_name].qcamera_path, + url if fn in FileName.RLOG else segments[segment_name].log_path, + url if fn in FileName.QLOG else segments[segment_name].qlog_path, + url if fn in FileName.FCAMERA else segments[segment_name].camera_path, + url if fn in FileName.DCAMERA else segments[segment_name].dcamera_path, + url if fn in FileName.ECAMERA else segments[segment_name].ecamera_path, + url if fn in FileName.QCAMERA else segments[segment_name].qcamera_path, self.metadata['url'], ) else: segments[segment_name] = Segment( segment_name, - url if fn in LOG_FILENAMES else None, - url if fn in QLOG_FILENAMES else None, - url if fn in CAMERA_FILENAMES else None, - url if fn in DCAMERA_FILENAMES else None, - url if fn in ECAMERA_FILENAMES else None, - url if fn in QCAMERA_FILENAMES else None, + url if fn in FileName.RLOG else None, + url if fn in FileName.QLOG else None, + url if fn in FileName.FCAMERA else None, + url if fn in FileName.DCAMERA else None, + url if fn in FileName.ECAMERA else None, + url if fn in FileName.QCAMERA else None, self.metadata['url'], ) @@ -136,32 +138,32 @@ class Route: for segment, files in segment_files.items(): try: - log_path = next(path for path, filename in files if filename in LOG_FILENAMES) + log_path = next(path for path, filename in files if filename in FileName.RLOG) except StopIteration: log_path = None try: - qlog_path = next(path for path, filename in files if filename in QLOG_FILENAMES) + qlog_path = next(path for path, filename in files if filename in FileName.QLOG) except StopIteration: qlog_path = None try: - camera_path = next(path for path, filename in files if filename in CAMERA_FILENAMES) + camera_path = next(path for path, filename in files if filename in FileName.FCAMERA) except StopIteration: camera_path = None try: - dcamera_path = next(path for path, filename in files if filename in DCAMERA_FILENAMES) + dcamera_path = next(path for path, filename in files if filename in FileName.DCAMERA) except StopIteration: dcamera_path = None try: - ecamera_path = next(path for path, filename in files if filename in ECAMERA_FILENAMES) + ecamera_path = next(path for path, filename in files if filename in FileName.ECAMERA) except StopIteration: ecamera_path = None try: - qcamera_path = next(path for path, filename in files if filename in QCAMERA_FILENAMES) + qcamera_path = next(path for path, filename in files if filename in FileName.QCAMERA) except StopIteration: qcamera_path = None diff --git a/tools/longitudinal_maneuvers/maneuversd.py b/tools/longitudinal_maneuvers/maneuversd.py index 6c6e252a5..c17ae2375 100755 --- a/tools/longitudinal_maneuvers/maneuversd.py +++ b/tools/longitudinal_maneuvers/maneuversd.py @@ -3,7 +3,7 @@ import numpy as np from dataclasses import dataclass from cereal import messaging, car -from opendbc.car.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.realtime import DT_MDL from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index d23052d0f..2a7f83baa 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -50,6 +50,7 @@ brew "zeromq" cask "gcc-arm-embedded" brew "portaudio" brew "gcc@13" +brew "font-noto-color-emoji" EOS echo "[ ] finished brew install t=$SECONDS" diff --git a/tools/plotjuggler/README.md b/tools/plotjuggler/README.md index 794c292e6..62905a252 100644 --- a/tools/plotjuggler/README.md +++ b/tools/plotjuggler/README.md @@ -13,14 +13,13 @@ Once you've [set up the openpilot environment](../README.md), this command will ``` $ ./juggle.py -h usage: juggle.py [-h] [--demo] [--can] [--stream] [--layout [LAYOUT]] [--install] [--dbc DBC] - [route_or_segment_name] [segment_count] + [route_or_segment_name] A helper to run PlotJuggler on openpilot routes positional arguments: route_or_segment_name The route or segment name to plot (cabana share URL accepted) (default: None) - segment_count The number of segments to plot (default: None) optional arguments: -h, --help show this help message and exit @@ -34,16 +33,20 @@ optional arguments: ``` -Examples using route name: +Example using route name: `./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19"` -Examples using segment range: +Examples using segment: `./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/1"` `./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/1/q" # use qlogs` +Example using segment range: + +`./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/0:1"` + ## Streaming Explore live data from your car! Follow these steps to stream from your comma device to your laptop: diff --git a/tools/replay/README.md b/tools/replay/README.md index 8b4afb0ac..72216e2cc 100644 --- a/tools/replay/README.md +++ b/tools/replay/README.md @@ -75,7 +75,7 @@ Options: --qcam load qcamera --no-hw-decoder disable HW video decoding --no-vipc do not output video - --all do output all messages including uiDebug, userFlag. + --all do output all messages including uiDebug, userBookmark. this may causes issues when used along with UI Arguments: @@ -91,14 +91,6 @@ tools/replay/replay cd selfdrive/ui && ./ui ``` -## Try Radar Point Visualization with Rerun -To visualize radar points, run rp_visualization.py while tools/replay/replay is active. - -```bash -tools/replay/replay -python3 replay/rp_visualization.py -``` - ## Work with plotjuggler If you want to use replay with plotjuggler, you can stream messages by running: diff --git a/tools/replay/consoleui.cc b/tools/replay/consoleui.cc index 871e41c27..185c7d6c3 100644 --- a/tools/replay/consoleui.cc +++ b/tools/replay/consoleui.cc @@ -257,7 +257,7 @@ void ConsoleUI::updateTimeline() { if (entry.type == TimelineType::Engaged) { mvwchgat(win, 1, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); mvwchgat(win, 2, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); - } else if (entry.type == TimelineType::UserFlag) { + } else if (entry.type == TimelineType::UserBookmark) { mvwchgat(win, 3, start_pos, end_pos - start_pos + 1, ACS_S3, Color::Cyan, NULL); } else { auto color_id = Color::Green; @@ -329,7 +329,7 @@ void ConsoleUI::handleKey(char c) { } else if (c == 'd') { replay->seekToFlag(FindFlag::nextDisEngagement); } else if (c == 't') { - replay->seekToFlag(FindFlag::nextUserFlag); + replay->seekToFlag(FindFlag::nextUserBookmark); } else if (c == 'i') { replay->seekToFlag(FindFlag::nextInfo); } else if (c == 'w') { diff --git a/tools/replay/lib/rp_helpers.py b/tools/replay/lib/rp_helpers.py deleted file mode 100644 index aa20ab8e3..000000000 --- a/tools/replay/lib/rp_helpers.py +++ /dev/null @@ -1,109 +0,0 @@ -import numpy as np -from openpilot.selfdrive.controls.radard import RADAR_TO_CAMERA - -# Color palette used for rerun AnnotationContext -rerunColorPalette = [(96, "red", (255, 0, 0)), - (100, "pink", (255, 36, 0)), - (124, "yellow", (255, 255, 0)), - (230, "vibrantpink", (255, 36, 170)), - (240, "orange", (255, 146, 0)), - (255, "white", (255, 255, 255)), - (110, "carColor", (255,0,127)), - (0, "background", (0, 0, 0))] - - -class UIParams: - lidar_x, lidar_y, lidar_zoom = 384, 960, 6 - lidar_car_x, lidar_car_y = lidar_x / 2., lidar_y / 1.1 - car_hwidth = 1.7272 / 2 * lidar_zoom - car_front = 2.6924 * lidar_zoom - car_back = 1.8796 * lidar_zoom - car_color = rerunColorPalette[6][0] -UP = UIParams - - -def to_topdown_pt(y, x): - px, py = x * UP.lidar_zoom + UP.lidar_car_x, -y * UP.lidar_zoom + UP.lidar_car_y - if px > 0 and py > 0 and px < UP.lidar_x and py < UP.lidar_y: - return int(px), int(py) - return -1, -1 - - -def draw_path(path, lid_overlay, lid_color=None): - x, y = np.asarray(path.x), np.asarray(path.y) - # draw lidar path point on lidar - if lid_color is not None and lid_overlay is not None: - for i in range(len(x)): - px, py = to_topdown_pt(x[i], y[i]) - if px != -1: - lid_overlay[px, py] = lid_color - - -def plot_model(m, lid_overlay): - if lid_overlay is None: - return - for lead in m.leadsV3: - if lead.prob < 0.5: - continue - x, y = lead.x[0], lead.y[0] - x_std = lead.xStd[0] - x -= RADAR_TO_CAMERA - _, py_top = to_topdown_pt(x + x_std, y) - px, py_bottom = to_topdown_pt(x - x_std, y) - lid_overlay[int(round(px - 4)):int(round(px + 4)), py_top:py_bottom] = rerunColorPalette[2][0] - - for path in m.laneLines: - draw_path(path, lid_overlay, rerunColorPalette[2][0]) - for edge in m.roadEdges: - draw_path(edge, lid_overlay, rerunColorPalette[0][0]) - draw_path(m.position, lid_overlay, rerunColorPalette[0][0]) - - -def plot_lead(rs, lid_overlay): - for lead in [rs.leadOne, rs.leadTwo]: - if not lead.status: - continue - x = lead.dRel - px_left, py = to_topdown_pt(x, -10) - px_right, _ = to_topdown_pt(x, 10) - lid_overlay[px_left:px_right, py] = rerunColorPalette[0][0] - - -def update_radar_points(lt, lid_overlay): - ar_pts = [] - if lt is not None: - ar_pts = {} - for track in lt: - ar_pts[track.trackId] = [track.dRel, track.yRel, track.vRel, track.aRel, track.oncoming, track.stationary] - for ids, pt in ar_pts.items(): - # negative here since radar is left positive - px, py = to_topdown_pt(pt[0], -pt[1]) - if px != -1: - if pt[-1]: - color = rerunColorPalette[4][0] - elif pt[-2]: - color = rerunColorPalette[3][0] - else: - color = rerunColorPalette[5][0] - if int(ids) == 1: - lid_overlay[px - 2:px + 2, py - 10:py + 10] = rerunColorPalette[1][0] - else: - lid_overlay[px - 2:px + 2, py - 2:py + 2] = color - - -def get_blank_lid_overlay(UP): - lid_overlay = np.zeros((UP.lidar_x, UP.lidar_y), 'uint8') - # Draw the car. - lid_overlay[int(round(UP.lidar_car_x - UP.car_hwidth)):int( - round(UP.lidar_car_x + UP.car_hwidth)), int(round(UP.lidar_car_y - - UP.car_front))] = UP.car_color - lid_overlay[int(round(UP.lidar_car_x - UP.car_hwidth)):int( - round(UP.lidar_car_x + UP.car_hwidth)), int(round(UP.lidar_car_y + - UP.car_back))] = UP.car_color - lid_overlay[int(round(UP.lidar_car_x - UP.car_hwidth)), int( - round(UP.lidar_car_y - UP.car_front)):int(round( - UP.lidar_car_y + UP.car_back))] = UP.car_color - lid_overlay[int(round(UP.lidar_car_x + UP.car_hwidth)), int( - round(UP.lidar_car_y - UP.car_front)):int(round( - UP.lidar_car_y + UP.car_back))] = UP.car_color - return lid_overlay diff --git a/tools/replay/main.cc b/tools/replay/main.cc index 38a1da292..f95098507 100644 --- a/tools/replay/main.cc +++ b/tools/replay/main.cc @@ -30,7 +30,7 @@ Options: --qcam Load qcamera --no-hw-decoder Disable HW video decoding --no-vipc Do not output video - --all Output all messages including uiDebug, userFlag + --all Output all messages including uiDebug, userBookmark -h, --help Show this help message )"; diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index a8e5cd9d4..7ecad8287 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -20,7 +20,7 @@ Replay::Replay(const std::string &route, std::vector allow, std::ve std::signal(SIGUSR1, interrupt_sleep_handler); if (!(flags_ & REPLAY_FLAG_ALL_SERVICES)) { - block.insert(block.end(), {"uiDebug", "userFlag"}); + block.insert(block.end(), {"uiDebug", "userBookmark"}); } setupServices(allow, block); setupSegmentManager(!allow.empty() || !block.empty()); diff --git a/tools/replay/rp_visualization.py b/tools/replay/rp_visualization.py deleted file mode 100755 index 25169b72a..000000000 --- a/tools/replay/rp_visualization.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import os -import sys -import numpy as np -import rerun as rr -import cereal.messaging as messaging -from openpilot.common.basedir import BASEDIR -from openpilot.tools.replay.lib.rp_helpers import (UP, rerunColorPalette, - get_blank_lid_overlay, - update_radar_points, plot_lead, - plot_model) -from msgq.visionipc import VisionIpcClient, VisionStreamType - -os.environ['BASEDIR'] = BASEDIR - -UP.lidar_zoom = 6 - -def visualize(addr): - sm = messaging.SubMaster(['radarState', 'liveTracks', 'modelV2'], addr=addr) - vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True) - while True: - if not vipc_client.is_connected(): - vipc_client.connect(True) - new_data = vipc_client.recv() - if new_data is None or not new_data.data.any(): - continue - - sm.update(0) - lid_overlay = get_blank_lid_overlay(UP) - if sm.recv_frame['modelV2']: - plot_model(sm['modelV2'], lid_overlay) - if sm.recv_frame['radarState']: - plot_lead(sm['radarState'], lid_overlay) - liveTracksTime = sm.logMonoTime['liveTracks'] - if sm.updated['liveTracks']: - update_radar_points(sm['liveTracks'], lid_overlay) - rr.set_time_nanos("TIMELINE", liveTracksTime) - rr.log("tracks", rr.SegmentationImage(np.flip(np.rot90(lid_overlay, k=-1), axis=1))) - - -def get_arg_parser(): - parser = argparse.ArgumentParser( - description="Show replay data in a UI.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("ip_address", nargs="?", default="127.0.0.1", - help="The ip address on which to receive zmq messages.") - parser.add_argument("--frame-address", default=None, - help="The frame address (fully qualified ZMQ endpoint for frames) on which to receive zmq messages.") - return parser - - -if __name__ == "__main__": - args = get_arg_parser().parse_args(sys.argv[1:]) - if args.ip_address != "127.0.0.1": - os.environ["ZMQ"] = "1" - messaging.reset_context() - rr.init("RadarPoints", spawn= True) - rr.log("tracks", rr.AnnotationContext(rerunColorPalette), static=True) - visualize(args.ip_address) diff --git a/tools/replay/timeline.cc b/tools/replay/timeline.cc index a4c2ffb70..39ea8b1be 100644 --- a/tools/replay/timeline.cc +++ b/tools/replay/timeline.cc @@ -26,7 +26,7 @@ std::optional Timeline::find(double cur_ts, FindFlag flag) const { return entry.end_time; } } else if (entry.start_time > cur_ts) { - if ((flag == FindFlag::nextUserFlag && entry.type == TimelineType::UserFlag) || + if ((flag == FindFlag::nextUserBookmark && entry.type == TimelineType::UserBookmark) || (flag == FindFlag::nextInfo && entry.type == TimelineType::AlertInfo) || (flag == FindFlag::nextWarning && entry.type == TimelineType::AlertWarning) || (flag == FindFlag::nextCritical && entry.type == TimelineType::AlertCritical)) { @@ -66,8 +66,8 @@ void Timeline::buildTimeline(const Route &route, uint64_t route_start_ts, bool l auto cs = reader.getRoot().getSelfdriveState(); updateEngagementStatus(cs, current_engaged_idx, seconds); updateAlertStatus(cs, current_alert_idx, seconds); - } else if (e.which == cereal::Event::Which::USER_FLAG) { - staging_entries_.emplace_back(Entry{seconds, seconds, TimelineType::UserFlag}); + } else if (e.which == cereal::Event::Which::USER_BOOKMARK) { + staging_entries_.emplace_back(Entry{seconds, seconds, TimelineType::UserBookmark}); } } diff --git a/tools/replay/timeline.h b/tools/replay/timeline.h index 74add9e5c..9688b6b95 100644 --- a/tools/replay/timeline.h +++ b/tools/replay/timeline.h @@ -7,8 +7,8 @@ #include "tools/replay/route.h" -enum class TimelineType { None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserFlag }; -enum class FindFlag { nextEngagement, nextDisEngagement, nextUserFlag, nextInfo, nextWarning, nextCritical }; +enum class TimelineType { None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserBookmark }; +enum class FindFlag { nextEngagement, nextDisEngagement, nextUserBookmark, nextInfo, nextWarning, nextCritical }; class Timeline { public: diff --git a/tools/replay/ui.py b/tools/replay/ui.py index 6e4057990..d71d63d1c 100755 --- a/tools/replay/ui.py +++ b/tools/replay/ui.py @@ -149,7 +149,7 @@ def ui_thread(addr): plot_arr[-1, name_to_arr_idx['angle_steers']] = sm['carState'].steeringAngleDeg plot_arr[-1, name_to_arr_idx['angle_steers_des']] = sm['carControl'].actuators.steeringAngleDeg plot_arr[-1, name_to_arr_idx['angle_steers_k']] = angle_steers_k - plot_arr[-1, name_to_arr_idx['gas']] = sm['carState'].gas + plot_arr[-1, name_to_arr_idx['gas']] = sm['carState'].gasDEPRECATED # TODO gas is deprecated plot_arr[-1, name_to_arr_idx['computer_gas']] = np.clip(sm['carControl'].actuators.accel/4.0, 0.0, 1.0) plot_arr[-1, name_to_arr_idx['user_brake']] = sm['carState'].brake diff --git a/tools/rerun/README.md b/tools/rerun/README.md deleted file mode 100644 index 11a8d33ee..000000000 --- a/tools/rerun/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Rerun -Rerun is a tool to quickly visualize time series data. It supports all openpilot logs , both the `logMessages` and video logs. - -[Instructions](https://rerun.io/docs/reference/viewer/overview) for navigation within the Rerun Viewer. - -## Usage -``` -usage: run.py [-h] [--demo] [--qcam] [--fcam] [--ecam] [--dcam] [route_or_segment_name] - -A helper to run rerun on openpilot routes - -positional arguments: - route_or_segment_name - The route or segment name to plot (default: None) - -options: - -h, --help show this help message and exit - --demo Use the demo route instead of providing one (default: False) - --qcam Show low-res road camera (default: False) - --fcam Show driving camera (default: False) - --ecam Show wide camera (default: False) - --dcam Show driver monitoring camera (default: False) -``` - -Examples using route name to observe accelerometer and qcamera: - -`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19"` - -Examples using segment range (more on [SegmentRange](https://github.com/commaai/openpilot/tree/master/tools/lib)): - -`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19/2:4"` - -## Cautions: -- Showing hevc videos (`--fcam`, `--ecam`, and `--dcam`) are expensive, and it's recommended to use `--qcam` for optimized performance. If possible, limiting your route to a few segments using `SegmentRange` will speed up logging and reduce memory usage - -## Demo -`./run.py --qcam --demo` diff --git a/tools/rerun/camera_reader.py b/tools/rerun/camera_reader.py deleted file mode 100644 index 8366a3c1c..000000000 --- a/tools/rerun/camera_reader.py +++ /dev/null @@ -1,93 +0,0 @@ -import tqdm -import subprocess -import multiprocessing -from enum import StrEnum -from functools import partial -from collections import namedtuple - -from openpilot.tools.lib.framereader import ffprobe - -CameraConfig = namedtuple("CameraConfig", ["qcam", "fcam", "ecam", "dcam"]) - -class CameraType(StrEnum): - qcam = "qcamera" - fcam = "fcamera" - ecam = "ecamera" - dcam = "dcamera" - - -def probe_packet_info(camera_path): - args = ["ffprobe", "-v", "quiet", "-show_packets", "-probesize", "10M", camera_path] - dat = subprocess.check_output(args) - dat = dat.decode().split() - return dat - - -class _FrameReader: - def __init__(self, camera_path, segment, h, w, start_time): - self.camera_path = camera_path - self.segment = segment - self.h = h - self.w = w - self.start_time = start_time - - self.ts = self._get_ts() - - def _read_stream_nv12(self): - frame_sz = self.w * self.h * 3 // 2 - proc = subprocess.Popen( - ["ffmpeg", "-v", "quiet", "-i", self.camera_path, "-f", "rawvideo", "-pix_fmt", "nv12", "-"], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL - ) - try: - while True: - dat = proc.stdout.read(frame_sz) - if len(dat) == 0: - break - yield dat - finally: - proc.kill() - - def _get_ts(self): - dat = probe_packet_info(self.camera_path) - try: - ret = [float(d.split('=')[1]) for d in dat if d.startswith("pts_time=")] - except ValueError: - # pts_times aren't available. Infer timestamps from duration_times - ret = [d for d in dat if d.startswith("duration_time")] - ret = [float(d.split('=')[1])*(i+1)+(self.segment*60)+self.start_time for i, d in enumerate(ret)] - return ret - - def __iter__(self): - for i, frame in enumerate(self._read_stream_nv12()): - yield self.ts[i], frame - - -class CameraReader: - def __init__(self, camera_paths, start_time, seg_idxs): - self.seg_idxs = seg_idxs - self.camera_paths = camera_paths - self.start_time = start_time - - probe = ffprobe(camera_paths[0])["streams"][0] - self.h = probe["height"] - self.w = probe["width"] - - self.__frs = {} - - def _get_fr(self, i): - if i not in self.__frs: - self.__frs[i] = _FrameReader(self.camera_paths[i], segment=i, h=self.h, w=self.w, start_time=self.start_time) - return self.__frs[i] - - def _run_on_segment(self, func, i): - return func(self._get_fr(i)) - - def run_across_segments(self, num_processes, func, desc=None): - with multiprocessing.Pool(num_processes) as pool: - num_segs = len(self.seg_idxs) - for _ in tqdm.tqdm(pool.imap_unordered(partial(self._run_on_segment, func), self.seg_idxs), total=num_segs, desc=desc): - continue - diff --git a/tools/rerun/run.py b/tools/rerun/run.py deleted file mode 100755 index 6ce8a3793..000000000 --- a/tools/rerun/run.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import argparse -import multiprocessing -import rerun as rr -import rerun.blueprint as rrb -from functools import partial -from collections import defaultdict - -from cereal.services import SERVICE_LIST -from openpilot.tools.rerun.camera_reader import probe_packet_info, CameraReader, CameraConfig, CameraType -from openpilot.tools.lib.logreader import LogReader -from openpilot.tools.lib.route import Route, SegmentRange - - -NUM_CPUS = multiprocessing.cpu_count() -DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" -RR_TIMELINE_NAME = "Timeline" -RR_WIN = "openpilot logs" - - -""" -Relevant upstream Rerun issues: -- loading videos directly: https://github.com/rerun-io/rerun/issues/6532 -""" - -class Rerunner: - def __init__(self, route, segment_range, camera_config): - self.lr = LogReader(route_or_segment_name) - - # hevc files don't have start_time. We get it from qcamera.ts - start_time = 0 - dat = probe_packet_info(route.qcamera_paths()[0]) - for d in dat: - if d.startswith("pts_time="): - start_time = float(d.split('=')[1]) - break - - qcam, fcam, ecam, dcam = camera_config - self.camera_readers = {} - if qcam: - self.camera_readers[CameraType.qcam] = CameraReader(route.qcamera_paths(), start_time, segment_range.seg_idxs) - if fcam: - self.camera_readers[CameraType.fcam] = CameraReader(route.camera_paths(), start_time, segment_range.seg_idxs) - if ecam: - self.camera_readers[CameraType.ecam] = CameraReader(route.ecamera_paths(), start_time, segment_range.seg_idxs) - if dcam: - self.camera_readers[CameraType.dcam] = CameraReader(route.dcamera_paths(), start_time, segment_range.seg_idxs) - - def _create_blueprint(self): - blueprint = None - service_views = [] - - for topic in sorted(SERVICE_LIST.keys()): - View = rrb.TimeSeriesView if topic != "thumbnail" else rrb.Spatial2DView - service_views.append(View(name=topic, origin=f"/{topic}/", visible=False)) - rr.log(topic, rr.SeriesLine(name=topic), timeless=True) - - center_view = [rrb.Vertical(*service_views, name="streams")] - if len(self.camera_readers): - center_view.append(rrb.Vertical(*[rrb.Spatial2DView(name=cam_type, origin=cam_type) for cam_type in self.camera_readers.keys()], name="cameras")) - - blueprint = rrb.Blueprint( - rrb.Horizontal( - *center_view - ), - rrb.SelectionPanel(expanded=False), - rrb.TimePanel(expanded=False), - ) - return blueprint - - @staticmethod - def _parse_msg(msg, parent_key=''): - stack = [(msg, parent_key)] - while stack: - current_msg, current_parent_key = stack.pop() - if isinstance(current_msg, list): - for index, item in enumerate(current_msg): - new_key = f"{current_parent_key}/{index}" - if isinstance(item, (int, float)): - yield new_key, item - elif isinstance(item, dict): - stack.append((item, new_key)) - elif isinstance(current_msg, dict): - for key, value in current_msg.items(): - new_key = f"{current_parent_key}/{key}" - if isinstance(value, (int, float)): - yield new_key, value - elif isinstance(value, dict): - stack.append((value, new_key)) - elif isinstance(value, list): - for index, item in enumerate(value): - if isinstance(item, (int, float)): - yield f"{new_key}/{index}", item - else: - pass # Not a plottable value - - @staticmethod - @rr.shutdown_at_exit - def _process_log_msgs(blueprint, lr): - rr.init(RR_WIN) - rr.connect() - rr.send_blueprint(blueprint) - - log_msgs = defaultdict(lambda: defaultdict(list)) - for msg in lr: - msg_type = msg.which() - - if msg_type == "thumbnail": - continue - - for entity_path, dat in Rerunner._parse_msg(msg.to_dict()[msg_type], msg_type): - log_msgs[entity_path]["times"].append(msg.logMonoTime) - log_msgs[entity_path]["data"].append(dat) - - for entity_path, log_msg in log_msgs.items(): - rr.send_columns( - entity_path, - times=[rr.TimeNanosColumn(RR_TIMELINE_NAME, log_msg["times"])], - components=[rr.components.ScalarBatch(log_msg["data"])] - ) - - return [] - - @staticmethod - @rr.shutdown_at_exit - def _process_cam_readers(blueprint, cam_type, h, w, fr): - rr.init(RR_WIN) - rr.connect() - rr.send_blueprint(blueprint) - - for ts, frame in fr: - rr.set_time_nanos(RR_TIMELINE_NAME, int(ts * 1e9)) - rr.log(cam_type, rr.Image(bytes=frame, width=w, height=h, pixel_format=rr.PixelFormat.NV12)) - - def load_data(self): - rr.init(RR_WIN, spawn=True) - - startup_blueprint = self._create_blueprint() - self.lr.run_across_segments(NUM_CPUS, partial(self._process_log_msgs, startup_blueprint), desc="Log messages") - for cam_type, cr in self.camera_readers.items(): - cr.run_across_segments(NUM_CPUS, partial(self._process_cam_readers, startup_blueprint, cam_type, cr.h, cr.w), desc=cam_type) - - rr.send_blueprint(self._create_blueprint()) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="A helper to run rerun on openpilot routes", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") - parser.add_argument("--qcam", action="store_true", help="Show low-res road camera") - parser.add_argument("--fcam", action="store_true", help="Show driving camera") - parser.add_argument("--ecam", action="store_true", help="Show wide camera") - parser.add_argument("--dcam", action="store_true", help="Show driver monitoring camera") - parser.add_argument("route_or_segment_name", nargs='?', help="The route or segment name to plot") - args = parser.parse_args() - - if not args.demo and not args.route_or_segment_name: - parser.print_help() - sys.exit() - - camera_config = CameraConfig(args.qcam, args.fcam, args.ecam, args.dcam) - route_or_segment_name = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip() - - sr = SegmentRange(route_or_segment_name) - r = Route(sr.route_name) - - hevc_requested = any(camera_config[1:]) - if len(sr.seg_idxs) > 1 and hevc_requested: - print("You're requesting more than 1 segment with hevc videos, " + \ - "please be aware that might take a lot of memory " + \ - "since rerun isn't yet well supported for high resolution video logging") - response = input("Do you wish to continue? (Y/n): ") - if response.strip().lower() != "y": - sys.exit() - - rerunner = Rerunner(r, sr, camera_config) - rerunner.load_data() - diff --git a/tools/sim/lib/simulated_car.py b/tools/sim/lib/simulated_car.py index ad0f4de5d..2681b2690 100644 --- a/tools/sim/lib/simulated_car.py +++ b/tools/sim/lib/simulated_car.py @@ -46,7 +46,7 @@ class SimulatedCar: msg.append(self.packer.make_can_msg("SCM_BUTTONS", 0, {"CRUISE_BUTTONS": simulator_state.cruise_button})) - msg.append(self.packer.make_can_msg("GEARBOX", 0, {"GEAR": 4, "GEAR_SHIFTER": 8})) + msg.append(self.packer.make_can_msg("GEARBOX_AUTO", 0, {"GEAR_SHIFTER": 4})) msg.append(self.packer.make_can_msg("GAS_PEDAL_2", 0, {})) msg.append(self.packer.make_can_msg("SEATBELT_STATUS", 0, {"SEATBELT_DRIVER_LATCHED": 1})) msg.append(self.packer.make_can_msg("STEER_STATUS", 0, {"STEER_TORQUE_SENSOR": simulator_state.user_torque})) @@ -56,7 +56,6 @@ class SimulatedCar: msg.append(self.packer.make_can_msg("STEER_MOTOR_TORQUE", 0, {})) msg.append(self.packer.make_can_msg("EPB_STATUS", 0, {})) msg.append(self.packer.make_can_msg("DOORS_STATUS", 0, {})) - msg.append(self.packer.make_can_msg("CRUISE_PARAMS", 0, {})) msg.append(self.packer.make_can_msg("CRUISE", 0, {})) msg.append(self.packer.make_can_msg("CRUISE_FAULT_STATUS", 0, {})) msg.append(self.packer.make_can_msg("SCM_FEEDBACK", 0, diff --git a/uv.lock b/uv.lock index 3b8de83bd..f1749bd9b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11, <3.13" resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'darwin'", @@ -166,7 +166,7 @@ wheels = [ [[package]] name = "azure-identity" -version = "1.23.1" +version = "1.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -175,9 +175,9 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/29/1201ffbb6a57a16524dd91f3e741b4c828a70aaba436578bdcb3fbcb438c/azure_identity-1.23.1.tar.gz", hash = "sha256:226c1ef982a9f8d5dcf6e0f9ed35eaef2a4d971e7dd86317e9b9d52e70a035e4", size = 266185, upload-time = "2025-07-15T19:16:38.077Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/44/f3ee20bacb220b6b4a2b0a6cf7e742eecb383a5ccf604dd79ec27c286b7e/azure_identity-1.24.0.tar.gz", hash = "sha256:6c3a40b2a70af831e920b89e6421e8dcd4af78a0cb38b9642d86c67643d4930c", size = 271630, upload-time = "2025-08-07T22:27:36.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/b3/e2d7ab810eb68575a5c7569b03c0228b8f4ce927ffa6211471b526f270c9/azure_identity-1.23.1-py3-none-any.whl", hash = "sha256:7eed28baa0097a47e3fb53bd35a63b769e6b085bb3cb616dfce2b67f28a004a1", size = 186810, upload-time = "2025-07-15T19:16:40.184Z" }, + { url = "https://files.pythonhosted.org/packages/a9/74/17428cb429e8d52f6d0d69ed685f4760a545cb0156594963a9337b53b6c9/azure_identity-1.24.0-py3-none-any.whl", hash = "sha256:9e04997cde0ab02ed66422c74748548e620b7b29361c72ce622acab0267ff7c4", size = 187890, upload-time = "2025-08-07T22:27:38.033Z" }, ] [[package]] @@ -220,11 +220,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.7.14" +version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] @@ -263,37 +263,33 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.2" +version = "3.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] [[package]] @@ -841,7 +837,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.3" +version = "3.10.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -854,20 +850,25 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873, upload-time = "2025-05-08T19:09:53.857Z" }, - { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205, upload-time = "2025-05-08T19:09:55.684Z" }, - { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823, upload-time = "2025-05-08T19:09:57.442Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464, upload-time = "2025-05-08T19:09:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103, upload-time = "2025-05-08T19:10:03.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492, upload-time = "2025-05-08T19:10:05.271Z" }, - { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689, upload-time = "2025-05-08T19:10:07.602Z" }, - { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466, upload-time = "2025-05-08T19:10:09.383Z" }, - { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252, upload-time = "2025-05-08T19:10:11.958Z" }, - { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload-time = "2025-05-08T19:10:14.47Z" }, - { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload-time = "2025-05-08T19:10:16.569Z" }, - { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload-time = "2025-05-08T19:10:18.663Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216, upload-time = "2025-07-31T18:07:51.947Z" }, + { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130, upload-time = "2025-07-31T18:07:53.65Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471, upload-time = "2025-07-31T18:07:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518, upload-time = "2025-07-31T18:07:57.199Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372, upload-time = "2025-07-31T18:07:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634, upload-time = "2025-07-31T18:08:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880, upload-time = "2025-07-31T18:08:03.407Z" }, + { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056, upload-time = "2025-07-31T18:08:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131, upload-time = "2025-07-31T18:08:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603, upload-time = "2025-07-31T18:08:09.064Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127, upload-time = "2025-07-31T18:08:10.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926, upload-time = "2025-07-31T18:08:13.186Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599, upload-time = "2025-07-31T18:08:15.116Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173, upload-time = "2025-07-31T18:08:21.518Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224, upload-time = "2025-07-31T18:09:27.512Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539, upload-time = "2025-07-31T18:09:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192, upload-time = "2025-07-31T18:09:31.407Z" }, ] [[package]] @@ -1064,28 +1065,28 @@ wheels = [ [[package]] name = "mypy" -version = "1.17.0" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, - { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, - { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, - { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, - { url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" }, - { url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" }, - { url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" }, - { url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" }, - { url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" }, - { url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" }, - { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, + { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, + { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, + { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, ] [[package]] @@ -4450,38 +4451,38 @@ wheels = [ [[package]] name = "pyzmq" -version = "27.0.0" +version = "27.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/06/50a4e9648b3e8b992bef8eb632e457307553a89d294103213cfd47b3da69/pyzmq-27.0.0.tar.gz", hash = "sha256:b1f08eeb9ce1510e6939b6e5dcd46a17765e2333daae78ecf4606808442e52cf", size = 280478, upload-time = "2025-06-13T14:09:07.087Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/5f/557d2032a2f471edbcc227da724c24a1c05887b5cda1e3ae53af98b9e0a5/pyzmq-27.0.1.tar.gz", hash = "sha256:45c549204bc20e7484ffd2555f6cf02e572440ecf2f3bdd60d4404b20fddf64b", size = 281158, upload-time = "2025-08-03T05:05:40.352Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/df/84c630654106d9bd9339cdb564aa941ed41b023a0264251d6743766bb50e/pyzmq-27.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:21457825249b2a53834fa969c69713f8b5a79583689387a5e7aed880963ac564", size = 1332718, upload-time = "2025-06-13T14:07:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8e/f6a5461a07654d9840d256476434ae0ff08340bba562a455f231969772cb/pyzmq-27.0.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1958947983fef513e6e98eff9cb487b60bf14f588dc0e6bf35fa13751d2c8251", size = 908248, upload-time = "2025-06-13T14:07:18.033Z" }, - { url = "https://files.pythonhosted.org/packages/7c/93/82863e8d695a9a3ae424b63662733ae204a295a2627d52af2f62c2cd8af9/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0dc628b5493f9a8cd9844b8bee9732ef587ab00002157c9329e4fc0ef4d3afa", size = 668647, upload-time = "2025-06-13T14:07:19.378Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/15278769b348121eacdbfcbd8c4d40f1102f32fa6af5be1ffc032ed684be/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7bbe9e1ed2c8d3da736a15694d87c12493e54cc9dc9790796f0321794bbc91f", size = 856600, upload-time = "2025-06-13T14:07:20.906Z" }, - { url = "https://files.pythonhosted.org/packages/d4/af/1c469b3d479bd095edb28e27f12eee10b8f00b356acbefa6aeb14dd295d1/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc1091f59143b471d19eb64f54bae4f54bcf2a466ffb66fe45d94d8d734eb495", size = 1657748, upload-time = "2025-06-13T14:07:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f4/17f965d0ee6380b1d6326da842a50e4b8b9699745161207945f3745e8cb5/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7011ade88c8e535cf140f8d1a59428676fbbce7c6e54fefce58bf117aefb6667", size = 2034311, upload-time = "2025-06-13T14:07:23.966Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6e/7c391d81fa3149fd759de45d298003de6cfab343fb03e92c099821c448db/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c386339d7e3f064213aede5d03d054b237937fbca6dd2197ac8cf3b25a6b14e", size = 1893630, upload-time = "2025-06-13T14:07:25.899Z" }, - { url = "https://files.pythonhosted.org/packages/0e/e0/eaffe7a86f60e556399e224229e7769b717f72fec0706b70ab2c03aa04cb/pyzmq-27.0.0-cp311-cp311-win32.whl", hash = "sha256:0546a720c1f407b2172cb04b6b094a78773491497e3644863cf5c96c42df8cff", size = 567706, upload-time = "2025-06-13T14:07:27.595Z" }, - { url = "https://files.pythonhosted.org/packages/c9/05/89354a8cffdcce6e547d48adaaf7be17007fc75572123ff4ca90a4ca04fc/pyzmq-27.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:15f39d50bd6c9091c67315ceb878a4f531957b121d2a05ebd077eb35ddc5efed", size = 630322, upload-time = "2025-06-13T14:07:28.938Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/4ab976d5e1e63976719389cc4f3bfd248a7f5f2bb2ebe727542363c61b5f/pyzmq-27.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c5817641eebb391a2268c27fecd4162448e03538387093cdbd8bf3510c316b38", size = 558435, upload-time = "2025-06-13T14:07:30.256Z" }, - { url = "https://files.pythonhosted.org/packages/93/a7/9ad68f55b8834ede477842214feba6a4c786d936c022a67625497aacf61d/pyzmq-27.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cbabc59dcfaac66655c040dfcb8118f133fb5dde185e5fc152628354c1598e52", size = 1305438, upload-time = "2025-06-13T14:07:31.676Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ee/26aa0f98665a22bc90ebe12dced1de5f3eaca05363b717f6fb229b3421b3/pyzmq-27.0.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cb0ac5179cba4b2f94f1aa208fbb77b62c4c9bf24dd446278b8b602cf85fcda3", size = 895095, upload-time = "2025-06-13T14:07:33.104Z" }, - { url = "https://files.pythonhosted.org/packages/cf/85/c57e7ab216ecd8aa4cc7e3b83b06cc4e9cf45c87b0afc095f10cd5ce87c1/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a48f0228eab6cbf69fde3aa3c03cbe04e50e623ef92ae395fce47ef8a76152", size = 651826, upload-time = "2025-06-13T14:07:34.831Z" }, - { url = "https://files.pythonhosted.org/packages/69/9a/9ea7e230feda9400fb0ae0d61d7d6ddda635e718d941c44eeab22a179d34/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111db5f395e09f7e775f759d598f43cb815fc58e0147623c4816486e1a39dc22", size = 839750, upload-time = "2025-06-13T14:07:36.553Z" }, - { url = "https://files.pythonhosted.org/packages/08/66/4cebfbe71f3dfbd417011daca267539f62ed0fbc68105357b68bbb1a25b7/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c8878011653dcdc27cc2c57e04ff96f0471e797f5c19ac3d7813a245bcb24371", size = 1641357, upload-time = "2025-06-13T14:07:38.21Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f6/b0f62578c08d2471c791287149cb8c2aaea414ae98c6e995c7dbe008adfb/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0ed2c1f335ba55b5fdc964622254917d6b782311c50e138863eda409fbb3b6d", size = 2020281, upload-time = "2025-06-13T14:07:39.599Z" }, - { url = "https://files.pythonhosted.org/packages/37/b9/4f670b15c7498495da9159edc374ec09c88a86d9cd5a47d892f69df23450/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e918d70862d4cfd4b1c187310015646a14e1f5917922ab45b29f28f345eeb6be", size = 1877110, upload-time = "2025-06-13T14:07:41.027Z" }, - { url = "https://files.pythonhosted.org/packages/66/31/9dee25c226295b740609f0d46db2fe972b23b6f5cf786360980524a3ba92/pyzmq-27.0.0-cp312-abi3-win32.whl", hash = "sha256:88b4e43cab04c3c0f0d55df3b1eef62df2b629a1a369b5289a58f6fa8b07c4f4", size = 559297, upload-time = "2025-06-13T14:07:42.533Z" }, - { url = "https://files.pythonhosted.org/packages/9b/12/52da5509800f7ff2d287b2f2b4e636e7ea0f001181cba6964ff6c1537778/pyzmq-27.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:dce4199bf5f648a902ce37e7b3afa286f305cd2ef7a8b6ec907470ccb6c8b371", size = 619203, upload-time = "2025-06-13T14:07:43.843Z" }, - { url = "https://files.pythonhosted.org/packages/93/6d/7f2e53b19d1edb1eb4f09ec7c3a1f945ca0aac272099eab757d15699202b/pyzmq-27.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:56e46bbb85d52c1072b3f809cc1ce77251d560bc036d3a312b96db1afe76db2e", size = 551927, upload-time = "2025-06-13T14:07:45.51Z" }, - { url = "https://files.pythonhosted.org/packages/98/a6/92394373b8dbc1edc9d53c951e8d3989d518185174ee54492ec27711779d/pyzmq-27.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd1dc59763effd1576f8368047c9c31468fce0af89d76b5067641137506792ae", size = 835948, upload-time = "2025-06-13T14:08:43.516Z" }, - { url = "https://files.pythonhosted.org/packages/56/f3/4dc38d75d9995bfc18773df3e41f2a2ca9b740b06f1a15dbf404077e7588/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:60e8cc82d968174650c1860d7b716366caab9973787a1c060cf8043130f7d0f7", size = 799874, upload-time = "2025-06-13T14:08:45.017Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ba/64af397e0f421453dc68e31d5e0784d554bf39013a2de0872056e96e58af/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14fe7aaac86e4e93ea779a821967360c781d7ac5115b3f1a171ced77065a0174", size = 567400, upload-time = "2025-06-13T14:08:46.855Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/ec956cbe98809270b59a22891d5758edae147a258e658bf3024a8254c855/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ad0562d4e6abb785be3e4dd68599c41be821b521da38c402bc9ab2a8e7ebc7e", size = 747031, upload-time = "2025-06-13T14:08:48.419Z" }, - { url = "https://files.pythonhosted.org/packages/be/8a/4a3764a68abc02e2fbb0668d225b6fda5cd39586dd099cee8b2ed6ab0452/pyzmq-27.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9df43a2459cd3a3563404c1456b2c4c69564daa7dbaf15724c09821a3329ce46", size = 544726, upload-time = "2025-06-13T14:08:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/a8e0da6ababbe9326116fb1c890bf1920eea880e8da621afb6bc0f39a262/pyzmq-27.0.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9729190bd770314f5fbba42476abf6abe79a746eeda11d1d68fd56dd70e5c296", size = 1332721, upload-time = "2025-08-03T05:03:15.237Z" }, + { url = "https://files.pythonhosted.org/packages/75/a4/9431ba598651d60ebd50dc25755402b770322cf8432adcc07d2906e53a54/pyzmq-27.0.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:696900ef6bc20bef6a242973943574f96c3f97d2183c1bd3da5eea4f559631b1", size = 908249, upload-time = "2025-08-03T05:03:16.933Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/e624e1793689e4e685d2ee21c40277dd4024d9d730af20446d88f69be838/pyzmq-27.0.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96a63aecec22d3f7fdea3c6c98df9e42973f5856bb6812c3d8d78c262fee808", size = 668649, upload-time = "2025-08-03T05:03:18.49Z" }, + { url = "https://files.pythonhosted.org/packages/6c/29/0652a39d4e876e0d61379047ecf7752685414ad2e253434348246f7a2a39/pyzmq-27.0.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c512824360ea7490390566ce00bee880e19b526b312b25cc0bc30a0fe95cb67f", size = 856601, upload-time = "2025-08-03T05:03:20.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/2d/8d5355d7fc55bb6e9c581dd74f58b64fa78c994079e3a0ea09b1b5627cde/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dfb2bb5e0f7198eaacfb6796fb0330afd28f36d985a770745fba554a5903595a", size = 1657750, upload-time = "2025-08-03T05:03:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f4/cd032352d5d252dc6f5ee272a34b59718ba3af1639a8a4ef4654f9535cf5/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4f6886c59ba93ffde09b957d3e857e7950c8fe818bd5494d9b4287bc6d5bc7f1", size = 2034312, upload-time = "2025-08-03T05:03:23.578Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1a/c050d8b6597200e97a4bd29b93c769d002fa0b03083858227e0376ad59bc/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b99ea9d330e86ce1ff7f2456b33f1bf81c43862a5590faf4ef4ed3a63504bdab", size = 1893632, upload-time = "2025-08-03T05:03:25.167Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/173ce21d5097e7fcf284a090e8beb64fc683c6582b1f00fa52b1b7e867ce/pyzmq-27.0.1-cp311-cp311-win32.whl", hash = "sha256:571f762aed89025ba8cdcbe355fea56889715ec06d0264fd8b6a3f3fa38154ed", size = 566587, upload-time = "2025-08-03T05:03:26.769Z" }, + { url = "https://files.pythonhosted.org/packages/53/ab/22bd33e7086f0a2cc03a5adabff4bde414288bb62a21a7820951ef86ec20/pyzmq-27.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee16906c8025fa464bea1e48128c048d02359fb40bebe5333103228528506530", size = 632873, upload-time = "2025-08-03T05:03:28.685Z" }, + { url = "https://files.pythonhosted.org/packages/90/14/3e59b4a28194285ceeff725eba9aa5ba8568d1cb78aed381dec1537c705a/pyzmq-27.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:ba068f28028849da725ff9185c24f832ccf9207a40f9b28ac46ab7c04994bd41", size = 558918, upload-time = "2025-08-03T05:03:30.085Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9b/c0957041067c7724b310f22c398be46399297c12ed834c3bc42200a2756f/pyzmq-27.0.1-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:af7ebce2a1e7caf30c0bb64a845f63a69e76a2fadbc1cac47178f7bb6e657bdd", size = 1305432, upload-time = "2025-08-03T05:03:32.177Z" }, + { url = "https://files.pythonhosted.org/packages/8e/55/bd3a312790858f16b7def3897a0c3eb1804e974711bf7b9dcb5f47e7f82c/pyzmq-27.0.1-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8f617f60a8b609a13099b313e7e525e67f84ef4524b6acad396d9ff153f6e4cd", size = 895095, upload-time = "2025-08-03T05:03:33.918Z" }, + { url = "https://files.pythonhosted.org/packages/20/50/fc384631d8282809fb1029a4460d2fe90fa0370a0e866a8318ed75c8d3bb/pyzmq-27.0.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d59dad4173dc2a111f03e59315c7bd6e73da1a9d20a84a25cf08325b0582b1a", size = 651826, upload-time = "2025-08-03T05:03:35.818Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0a/2356305c423a975000867de56888b79e44ec2192c690ff93c3109fd78081/pyzmq-27.0.1-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5b6133c8d313bde8bd0d123c169d22525300ff164c2189f849de495e1344577", size = 839751, upload-time = "2025-08-03T05:03:37.265Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1b/81e95ad256ca7e7ccd47f5294c1c6da6e2b64fbace65b84fe8a41470342e/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:58cca552567423f04d06a075f4b473e78ab5bdb906febe56bf4797633f54aa4e", size = 1641359, upload-time = "2025-08-03T05:03:38.799Z" }, + { url = "https://files.pythonhosted.org/packages/50/63/9f50ec965285f4e92c265c8f18344e46b12803666d8b73b65d254d441435/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:4b9d8e26fb600d0d69cc9933e20af08552e97cc868a183d38a5c0d661e40dfbb", size = 2020281, upload-time = "2025-08-03T05:03:40.338Z" }, + { url = "https://files.pythonhosted.org/packages/02/4a/19e3398d0dc66ad2b463e4afa1fc541d697d7bc090305f9dfb948d3dfa29/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2329f0c87f0466dce45bba32b63f47018dda5ca40a0085cc5c8558fea7d9fc55", size = 1877112, upload-time = "2025-08-03T05:03:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/bf/42/c562e9151aa90ed1d70aac381ea22a929d6b3a2ce4e1d6e2e135d34fd9c6/pyzmq-27.0.1-cp312-abi3-win32.whl", hash = "sha256:57bb92abdb48467b89c2d21da1ab01a07d0745e536d62afd2e30d5acbd0092eb", size = 558177, upload-time = "2025-08-03T05:03:43.979Z" }, + { url = "https://files.pythonhosted.org/packages/40/96/5c50a7d2d2b05b19994bf7336b97db254299353dd9b49b565bb71b485f03/pyzmq-27.0.1-cp312-abi3-win_amd64.whl", hash = "sha256:ff3f8757570e45da7a5bedaa140489846510014f7a9d5ee9301c61f3f1b8a686", size = 618923, upload-time = "2025-08-03T05:03:45.438Z" }, + { url = "https://files.pythonhosted.org/packages/13/33/1ec89c8f21c89d21a2eaff7def3676e21d8248d2675705e72554fb5a6f3f/pyzmq-27.0.1-cp312-abi3-win_arm64.whl", hash = "sha256:df2c55c958d3766bdb3e9d858b911288acec09a9aab15883f384fc7180df5bed", size = 552358, upload-time = "2025-08-03T05:03:46.887Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1a/49f66fe0bc2b2568dd4280f1f520ac8fafd73f8d762140e278d48aeaf7b9/pyzmq-27.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7fb0ee35845bef1e8c4a152d766242164e138c239e3182f558ae15cb4a891f94", size = 835949, upload-time = "2025-08-03T05:05:13.798Z" }, + { url = "https://files.pythonhosted.org/packages/49/94/443c1984b397eab59b14dd7ae8bc2ac7e8f32dbc646474453afcaa6508c4/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f379f11e138dfd56c3f24a04164f871a08281194dd9ddf656a278d7d080c8ad0", size = 799875, upload-time = "2025-08-03T05:05:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/30/f1/fd96138a0f152786a2ba517e9c6a8b1b3516719e412a90bb5d8eea6b660c/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b978c0678cffbe8860ec9edc91200e895c29ae1ac8a7085f947f8e8864c489fb", size = 567403, upload-time = "2025-08-03T05:05:17.326Z" }, + { url = "https://files.pythonhosted.org/packages/16/57/34e53ef2b55b1428dac5aabe3a974a16c8bda3bf20549ba500e3ff6cb426/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ebccf0d760bc92a4a7c751aeb2fef6626144aace76ee8f5a63abeb100cae87f", size = 747032, upload-time = "2025-08-03T05:05:19.074Z" }, + { url = "https://files.pythonhosted.org/packages/81/b7/769598c5ae336fdb657946950465569cf18803140fe89ce466d7f0a57c11/pyzmq-27.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:77fed80e30fa65708546c4119840a46691290efc231f6bfb2ac2a39b52e15811", size = 544566, upload-time = "2025-08-03T05:05:20.798Z" }, ] [[package]] @@ -4575,36 +4576,36 @@ wheels = [ [[package]] name = "rubicon-objc" -version = "0.5.1" +version = "0.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/83/e57741dcf862a2581d53eccf8b11749c97f73d9754bbc538fb6c7b527da3/rubicon_objc-0.5.1.tar.gz", hash = "sha256:90bee9fc1de4515e17615e15648989b88bb8d4d2ffc8c7c52748272cd7f30a66", size = 174639, upload-time = "2025-06-03T06:33:50.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/2f/612049b8601e810080f63f4b85acbf2ed0784349088d3c944eb288e1d487/rubicon_objc-0.5.2.tar.gz", hash = "sha256:1180593935f6a8a39c23b5f4b7baa24aedf9f7285e80804a1d9d6b50a50572f5", size = 175710, upload-time = "2025-08-07T06:12:25.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/0a/e451c3dbda38dd6abab1fd16c3b35623fc0635dffcbbf97f1acc55a58508/rubicon_objc-0.5.1-py3-none-any.whl", hash = "sha256:17092756241b8370231cfaad45ad6e8ce99534987f2acbc944d65df5bdf8f6cd", size = 63323, upload-time = "2025-06-03T06:33:48.863Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/11ad29100060af56408ed084dca76a16d2ce8eb31b75081bfc0eec45d755/rubicon_objc-0.5.2-py3-none-any.whl", hash = "sha256:829b253c579e51fc34f4bb6587c34806e78960dcc1eb24e62b38141a1fe02b39", size = 63512, upload-time = "2025-08-07T06:12:23.766Z" }, ] [[package]] name = "ruff" -version = "0.12.7" +version = "0.12.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/81/0bd3594fa0f690466e41bd033bdcdf86cba8288345ac77ad4afbe5ec743a/ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71", size = 5197814, upload-time = "2025-07-29T22:32:35.877Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/d2/6cb35e9c85e7a91e8d22ab32ae07ac39cc34a71f1009a6f9e4a2a019e602/ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303", size = 11852189, upload-time = "2025-07-29T22:31:41.281Z" }, - { url = "https://files.pythonhosted.org/packages/63/5b/a4136b9921aa84638f1a6be7fb086f8cad0fde538ba76bda3682f2599a2f/ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb", size = 12519389, upload-time = "2025-07-29T22:31:54.265Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c9/3e24a8472484269b6b1821794141f879c54645a111ded4b6f58f9ab0705f/ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3", size = 11743384, upload-time = "2025-07-29T22:31:59.575Z" }, - { url = "https://files.pythonhosted.org/packages/26/7c/458dd25deeb3452c43eaee853c0b17a1e84169f8021a26d500ead77964fd/ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860", size = 11943759, upload-time = "2025-07-29T22:32:01.95Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8b/658798472ef260ca050e400ab96ef7e85c366c39cf3dfbef4d0a46a528b6/ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c", size = 11654028, upload-time = "2025-07-29T22:32:04.367Z" }, - { url = "https://files.pythonhosted.org/packages/a8/86/9c2336f13b2a3326d06d39178fd3448dcc7025f82514d1b15816fe42bfe8/ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423", size = 13225209, upload-time = "2025-07-29T22:32:06.952Z" }, - { url = "https://files.pythonhosted.org/packages/76/69/df73f65f53d6c463b19b6b312fd2391dc36425d926ec237a7ed028a90fc1/ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb", size = 14182353, upload-time = "2025-07-29T22:32:10.053Z" }, - { url = "https://files.pythonhosted.org/packages/58/1e/de6cda406d99fea84b66811c189b5ea139814b98125b052424b55d28a41c/ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd", size = 13631555, upload-time = "2025-07-29T22:32:12.644Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ae/625d46d5164a6cc9261945a5e89df24457dc8262539ace3ac36c40f0b51e/ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e", size = 12667556, upload-time = "2025-07-29T22:32:15.312Z" }, - { url = "https://files.pythonhosted.org/packages/55/bf/9cb1ea5e3066779e42ade8d0cd3d3b0582a5720a814ae1586f85014656b6/ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606", size = 12939784, upload-time = "2025-07-29T22:32:17.69Z" }, - { url = "https://files.pythonhosted.org/packages/55/7f/7ead2663be5627c04be83754c4f3096603bf5e99ed856c7cd29618c691bd/ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8", size = 11771356, upload-time = "2025-07-29T22:32:20.134Z" }, - { url = "https://files.pythonhosted.org/packages/17/40/a95352ea16edf78cd3a938085dccc55df692a4d8ba1b3af7accbe2c806b0/ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa", size = 11612124, upload-time = "2025-07-29T22:32:22.645Z" }, - { url = "https://files.pythonhosted.org/packages/4d/74/633b04871c669e23b8917877e812376827c06df866e1677f15abfadc95cb/ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5", size = 12479945, upload-time = "2025-07-29T22:32:24.765Z" }, - { url = "https://files.pythonhosted.org/packages/be/34/c3ef2d7799c9778b835a76189c6f53c179d3bdebc8c65288c29032e03613/ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4", size = 12998677, upload-time = "2025-07-29T22:32:27.022Z" }, - { url = "https://files.pythonhosted.org/packages/77/ab/aca2e756ad7b09b3d662a41773f3edcbd262872a4fc81f920dc1ffa44541/ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77", size = 11756687, upload-time = "2025-07-29T22:32:29.381Z" }, - { url = "https://files.pythonhosted.org/packages/b4/71/26d45a5042bc71db22ddd8252ca9d01e9ca454f230e2996bb04f16d72799/ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f", size = 12912365, upload-time = "2025-07-29T22:32:31.517Z" }, - { url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, + { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, + { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, + { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, + { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, + { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, ] [[package]] @@ -4618,15 +4619,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.34.0" +version = "2.34.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/05/546f8b9baa303b9e9b38feab79222935d0a279f0ed4d2e2cb6e5a0963055/sentry_sdk-2.34.0.tar.gz", hash = "sha256:a024baf3bb229d4b482cb58e9755c212a157813a655f186060533e75a72240ea", size = 336952, upload-time = "2025-07-29T12:47:26.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/38/10d6bfe23df1bfc65ac2262ed10b45823f47f810b0057d3feeea1ca5c7ed/sentry_sdk-2.34.1.tar.gz", hash = "sha256:69274eb8c5c38562a544c3e9f68b5be0a43be4b697f5fd385bf98e4fbe672687", size = 336969, upload-time = "2025-07-30T11:13:37.93Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/d4/999d63debd1e53f18c95861ae425dcd4fca1e8ec1934c5906ca8da44e867/sentry_sdk-2.34.0-py2.py3-none-any.whl", hash = "sha256:1c9856d0666c112f3a7a749aba09821e79871b3e7d322833840e9358b8c71a60", size = 357707, upload-time = "2025-07-29T12:47:24.599Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3e/bb34de65a5787f76848a533afbb6610e01fbcdd59e76d8679c254e02255c/sentry_sdk-2.34.1-py2.py3-none-any.whl", hash = "sha256:b7a072e1cdc5abc48101d5146e1ae680fa81fe886d8d95aaa25a0b450c818d32", size = 357743, upload-time = "2025-07-30T11:13:36.145Z" }, ] [[package]] @@ -4800,14 +4801,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.32.4.20250611" +version = "2.32.4.20250809" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/b0/9355adb86ec84d057fea765e4c49cce592aaf3d5117ce5609a95a7fc3dac/types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3", size = 23027, upload-time = "2025-08-09T03:17:10.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6f/ec0012be842b1d888d46884ac5558fd62aeae1f0ec4f7a581433d890d4b5/types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163", size = 20644, upload-time = "2025-08-09T03:17:09.716Z" }, ] [[package]]