diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 65d2f5588e..e8f1b7eb8b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,11 @@ * @sunnypilot/dev-internal /.github/ @devtekve @sunnyhaibin -/release/ci/ @devtekve @sunnyhaibin \ No newline at end of file +/release/ci/ @devtekve @sunnyhaibin +/tinygrad_repo @devtekve @Discountchubbs +/tinygrad/ @devtekve @Discountchubbs +/selfdrive/controls/lib/longitudinal_planner.py @devtekve @Discountchubbs +/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @devtekve @Discountchubbs +/selfdrive/modeld/ @devtekve @Discountchubbs +/sunnypilot/model* @devtekve @Discountchubbs +/sunnypilot/sunnylink/ @devtekve +/system/athena/ @devtekve \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 99d42c36e3..d2228a2079 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -8,8 +8,7 @@ jobs: build_masterci: name: build master-ci env: - TARGET_DIR: /tmp/openpilot - ImageOS: ubuntu20 + ImageOS: ubuntu24 container: image: ghcr.io/commaai/openpilot-base:latest runs-on: ubuntu-latest @@ -23,7 +22,7 @@ jobs: sudo apt-get update sudo apt-get install -y libyaml-dev - name: Wait for green check mark - if: ${{ github.event_name != 'workflow_dispatch' }} + if: ${{ github.event_name == 'schedule' }} uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc with: ref: master @@ -39,16 +38,5 @@ jobs: run: | git config --global --add safe.directory '*' git lfs pull - - name: Build master-ci - run: | - release/build_devel.sh - - name: Run tests - run: | - export PYTHONPATH=$TARGET_DIR - cd $TARGET_DIR - scons -j$(nproc) - pytest -n logical selfdrive/car/tests/test_car_interfaces.py - name: Push master-ci - run: | - unset TARGET_DIR - BRANCH=__nightly release/build_devel.sh + run: BRANCH=__nightly release/build_devel.sh diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 8b0db6ce88..6bd86ce7e9 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -54,8 +54,9 @@ jobs: git add . - name: update car docs run: | + export PYTHONPATH="$PWD" scons -j$(nproc) --minimal opendbc_repo - PYTHONPATH=. python selfdrive/car/docs.py + python selfdrive/car/docs.py git add docs/CARS.md - name: Create Pull Request uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 diff --git a/Dockerfile.openpilot b/Dockerfile.openpilot index f34caba81b..d85be77121 100644 --- a/Dockerfile.openpilot +++ b/Dockerfile.openpilot @@ -3,7 +3,6 @@ FROM ghcr.io/commaai/openpilot-base:latest ENV PYTHONUNBUFFERED=1 ENV OPENPILOT_PATH=/home/batman/openpilot -ENV PYTHONPATH=${OPENPILOT_PATH}:${PYTHONPATH} RUN mkdir -p ${OPENPILOT_PATH} WORKDIR ${OPENPILOT_PATH} diff --git a/RELEASES.md b/RELEASES.md index acdb709c9b..91652d6dc7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,8 +1,13 @@ +Version 0.9.10 (2025-06-30) +======================== + Version 0.9.9 (2025-05-23) ======================== * New driving model * New training architecture using parts from MLSIM * Steering actuation delay is now learned online +* Ford Escape 2023-24 support thanks to incognitojam! +* Ford Kuga 2024 support thanks to incognitojam! * Hyundai Nexo 2021 support thanks to sunnyhaibin! * Tesla Model 3 and Y support thanks to lukasloetkolben! * Lexus RC 2023 support thanks to nelsonjchen! diff --git a/SConstruct b/SConstruct index f62c5b13af..77c8525896 100644 --- a/SConstruct +++ b/SConstruct @@ -363,14 +363,13 @@ SConscript(['rednose/SConscript']) # Build system services SConscript([ - 'system/proclogd/SConscript', 'system/ubloxd/SConscript', 'system/loggerd/SConscript', ]) if arch != "Darwin": SConscript([ - 'system/sensord/SConscript', 'system/logcatd/SConscript', + 'system/proclogd/SConscript', ]) if arch == "larch64": diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 8ae77875f2..c9e4f3e16d 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -223,10 +223,16 @@ struct BackupManagerSP @0xf98d843bfd7004a3 { } } -struct CustomReserved7 @0xb86e6369214c01c8 { +struct CarStateSP @0xb86e6369214c01c8 { } -struct CustomReserved8 @0xf416ec09499d9d19 { +struct LiveMapDataSP @0xf416ec09499d9d19 { + speedLimitValid @0 :Bool; + speedLimit @1 :Float32; + speedLimitAheadValid @2 :Bool; + speedLimitAhead @3 :Float32; + speedLimitAheadDistance @4 :Float32; + roadName @5 :Text; } struct CustomReserved9 @0xa1680744031fdb2d { diff --git a/cereal/log.capnp b/cereal/log.capnp index 79f11f6bcf..7c7983d147 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2281,6 +2281,7 @@ struct LiveTorqueParametersData { points @10 :List(List(Float32)); version @11 :Int32; useParams @12 :Bool; + calPerc @13 :Int8; } struct LiveDelayData { @@ -2291,6 +2292,7 @@ struct LiveDelayData { lateralDelayEstimate @3 :Float32; lateralDelayEstimateStd @5 :Float32; points @4 :List(Float32); + calPerc @6 :Int8; enum Status { unestimated @0; @@ -2609,8 +2611,8 @@ struct Event { carParamsSP @111 :Custom.CarParamsSP; carControlSP @112 :Custom.CarControlSP; backupManagerSP @113 :Custom.BackupManagerSP; - customReserved7 @114 :Custom.CustomReserved7; - customReserved8 @115 :Custom.CustomReserved8; + carStateSP @114 :Custom.CarStateSP; + liveMapDataSP @115 :Custom.LiveMapDataSP; customReserved9 @116 :Custom.CustomReserved9; customReserved10 @136 :Custom.CustomReserved10; customReserved11 @137 :Custom.CustomReserved11; diff --git a/cereal/services.py b/cereal/services.py index c40538eec0..3e72041b72 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -83,6 +83,8 @@ _services: dict[str, tuple] = { "onroadEventsSP": (True, 1., 1), "carParamsSP": (True, 0.02, 1), "carControlSP": (True, 100., 10), + "carStateSP": (True, 100., 10), + "liveMapDataSP": (True, 1., 1), # debug "uiDebug": (True, 0., 1), diff --git a/common/SConscript b/common/SConscript index 829db6eeec..3cdb6fc5a2 100644 --- a/common/SConscript +++ b/common/SConscript @@ -4,14 +4,10 @@ common_libs = [ 'params.cc', 'swaglog.cc', 'util.cc', - 'i2c.cc', 'watchdog.cc', 'ratekeeper.cc' ] -if arch != "Darwin": - common_libs.append('gpio.cc') - _common = env.Library('common', common_libs, LIBS="json11") files = [ diff --git a/common/gpio.cc b/common/gpio.cc deleted file mode 100644 index dd7ba34b6d..0000000000 --- a/common/gpio.cc +++ /dev/null @@ -1,84 +0,0 @@ -#include "common/gpio.h" - -#include - -#ifdef __APPLE__ -int gpio_init(int pin_nr, bool output) { - return 0; -} - -int gpio_set(int pin_nr, bool high) { - return 0; -} - -int gpiochip_get_ro_value_fd(const char* consumer_label, int gpiochiop_id, int pin_nr) { - return 0; -} - -#else - -#include -#include - -#include -#include -#include - -#include "common/util.h" -#include "common/swaglog.h" - -int gpio_init(int pin_nr, bool output) { - char pin_dir_path[50]; - int pin_dir_path_len = snprintf(pin_dir_path, sizeof(pin_dir_path), - "/sys/class/gpio/gpio%d/direction", pin_nr); - if (pin_dir_path_len <= 0) { - return -1; - } - const char *value = output ? "out" : "in"; - return util::write_file(pin_dir_path, (void*)value, strlen(value)); -} - -int gpio_set(int pin_nr, bool high) { - char pin_val_path[50]; - int pin_val_path_len = snprintf(pin_val_path, sizeof(pin_val_path), - "/sys/class/gpio/gpio%d/value", pin_nr); - if (pin_val_path_len <= 0) { - return -1; - } - return util::write_file(pin_val_path, (void*)(high ? "1" : "0"), 1); -} - -int gpiochip_get_ro_value_fd(const char* consumer_label, int gpiochiop_id, int pin_nr) { - - // Assumed that all interrupt pins are unexported and rights are given to - // read from gpiochip0. - std::string gpiochip_path = "/dev/gpiochip" + std::to_string(gpiochiop_id); - int fd = open(gpiochip_path.c_str(), O_RDONLY); - if (fd < 0) { - LOGE("Error opening gpiochip0 fd"); - return -1; - } - - // Setup event - struct gpioevent_request rq; - rq.lineoffset = pin_nr; - rq.handleflags = GPIOHANDLE_REQUEST_INPUT; - - /* Requesting both edges as the data ready pulse from the lsm6ds sensor is - very short(75us) and is mostly detected as falling edge instead of rising. - So if it is detected as rising the following falling edge is skipped. */ - rq.eventflags = GPIOEVENT_REQUEST_BOTH_EDGES; - - strncpy(rq.consumer_label, consumer_label, std::size(rq.consumer_label) - 1); - int ret = util::safe_ioctl(fd, GPIO_GET_LINEEVENT_IOCTL, &rq); - if (ret == -1) { - LOGE("Unable to get line event from ioctl : %s", strerror(errno)); - close(fd); - return -1; - } - - close(fd); - return rq.fd; -} - -#endif diff --git a/common/gpio.h b/common/gpio.h deleted file mode 100644 index 89cdedd66c..0000000000 --- a/common/gpio.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -// Pin definitions -#ifdef QCOM2 - #define GPIO_HUB_RST_N 30 - #define GPIO_UBLOX_RST_N 32 - #define GPIO_UBLOX_SAFEBOOT_N 33 - #define GPIO_GNSS_PWR_EN 34 /* SCHEMATIC LABEL: GPIO_UBLOX_PWR_EN */ - #define GPIO_STM_RST_N 124 - #define GPIO_STM_BOOT0 134 - #define GPIO_BMX_ACCEL_INT 21 - #define GPIO_BMX_GYRO_INT 23 - #define GPIO_BMX_MAGN_INT 87 - #define GPIO_LSM_INT 84 - #define GPIOCHIP_INT 0 -#else - #define GPIO_HUB_RST_N 0 - #define GPIO_UBLOX_RST_N 0 - #define GPIO_UBLOX_SAFEBOOT_N 0 - #define GPIO_GNSS_PWR_EN 0 /* SCHEMATIC LABEL: GPIO_UBLOX_PWR_EN */ - #define GPIO_STM_RST_N 0 - #define GPIO_STM_BOOT0 0 - #define GPIO_BMX_ACCEL_INT 0 - #define GPIO_BMX_GYRO_INT 0 - #define GPIO_BMX_MAGN_INT 0 - #define GPIO_LSM_INT 0 - #define GPIOCHIP_INT 0 -#endif - -int gpio_init(int pin_nr, bool output); -int gpio_set(int pin_nr, bool high); - -int gpiochip_get_ro_value_fd(const char* consumer_label, int gpiochiop_id, int pin_nr); diff --git a/common/gpio.py b/common/gpio.py index 66b2adf438..8f025a2daf 100644 --- a/common/gpio.py +++ b/common/gpio.py @@ -1,4 +1,6 @@ import os +import fcntl +import ctypes from functools import cache def gpio_init(pin: int, output: bool) -> None: @@ -52,3 +54,36 @@ def get_irqs_for_action(action: str) -> list[str]: if irq.isdigit() and action in get_irq_action(irq): ret.append(irq) return ret + +# *** gpiochip *** + +class gpioevent_data(ctypes.Structure): + _fields_ = [ + ("timestamp", ctypes.c_uint64), + ("id", ctypes.c_uint32), + ] + +class gpioevent_request(ctypes.Structure): + _fields_ = [ + ("lineoffset", ctypes.c_uint32), + ("handleflags", ctypes.c_uint32), + ("eventflags", ctypes.c_uint32), + ("label", ctypes.c_char * 32), + ("fd", ctypes.c_int) + ] + +def gpiochip_get_ro_value_fd(label: str, gpiochip_id: int, pin: int) -> int: + GPIOEVENT_REQUEST_BOTH_EDGES = 0x3 + GPIOHANDLE_REQUEST_INPUT = 0x1 + GPIO_GET_LINEEVENT_IOCTL = 0xc030b404 + + rq = gpioevent_request() + rq.lineoffset = pin + rq.handleflags = GPIOHANDLE_REQUEST_INPUT + rq.eventflags = GPIOEVENT_REQUEST_BOTH_EDGES + rq.label = label.encode('utf-8')[:31] + b'\0' + + fd = os.open(f"/dev/gpiochip{gpiochip_id}", os.O_RDONLY) + fcntl.ioctl(fd, GPIO_GET_LINEEVENT_IOCTL, rq) + os.close(fd) + return int(rq.fd) diff --git a/common/i2c.cc b/common/i2c.cc deleted file mode 100644 index 3d6c79efc5..0000000000 --- a/common/i2c.cc +++ /dev/null @@ -1,92 +0,0 @@ -#include "common/i2c.h" - -#include -#include -#include - -#include -#include -#include - -#include "common/swaglog.h" -#include "common/util.h" - -#define UNUSED(x) (void)(x) - -#ifdef QCOM2 -// TODO: decide if we want to install libi2c-dev everywhere -extern "C" { - #include - #include -} - -I2CBus::I2CBus(uint8_t bus_id) { - char bus_name[20]; - snprintf(bus_name, 20, "/dev/i2c-%d", bus_id); - - i2c_fd = HANDLE_EINTR(open(bus_name, O_RDWR)); - if (i2c_fd < 0) { - throw std::runtime_error("Failed to open I2C bus"); - } -} - -I2CBus::~I2CBus() { - if (i2c_fd >= 0) { - close(i2c_fd); - } -} - -int I2CBus::read_register(uint8_t device_address, uint register_address, uint8_t *buffer, uint8_t len) { - std::lock_guard lk(m); - - int ret = 0; - - ret = HANDLE_EINTR(ioctl(i2c_fd, I2C_SLAVE, device_address)); - if (ret < 0) { goto fail; } - - ret = i2c_smbus_read_i2c_block_data(i2c_fd, register_address, len, buffer); - if ((ret < 0) || (ret != len)) { goto fail; } - -fail: - return ret; -} - -int I2CBus::set_register(uint8_t device_address, uint register_address, uint8_t data) { - std::lock_guard lk(m); - - int ret = 0; - - ret = HANDLE_EINTR(ioctl(i2c_fd, I2C_SLAVE, device_address)); - if (ret < 0) { goto fail; } - - ret = i2c_smbus_write_byte_data(i2c_fd, register_address, data); - if (ret < 0) { goto fail; } - -fail: - return ret; -} - -#else - -I2CBus::I2CBus(uint8_t bus_id) { - UNUSED(bus_id); - i2c_fd = -1; -} - -I2CBus::~I2CBus() {} - -int I2CBus::read_register(uint8_t device_address, uint register_address, uint8_t *buffer, uint8_t len) { - UNUSED(device_address); - UNUSED(register_address); - UNUSED(buffer); - UNUSED(len); - return -1; -} - -int I2CBus::set_register(uint8_t device_address, uint register_address, uint8_t data) { - UNUSED(device_address); - UNUSED(register_address); - UNUSED(data); - return -1; -} -#endif diff --git a/common/i2c.h b/common/i2c.h deleted file mode 100644 index ca0d4635b8..0000000000 --- a/common/i2c.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include - -#include - -class I2CBus { - private: - int i2c_fd; - std::mutex m; - - public: - I2CBus(uint8_t bus_id); - ~I2CBus(); - - int read_register(uint8_t device_address, uint register_address, uint8_t *buffer, uint8_t len); - int set_register(uint8_t device_address, uint register_address, uint8_t data); -}; diff --git a/common/logging_extra.py b/common/logging_extra.py index e921b140c2..ceaf083cd5 100644 --- a/common/logging_extra.py +++ b/common/logging_extra.py @@ -199,7 +199,6 @@ class SwagLogger(logging.Logger): co = f.f_code filename = os.path.normcase(co.co_filename) - # TODO: is this pylint exception correct? if filename == _srcfile: f = f.f_back continue diff --git a/common/model.h b/common/model.h index ac01b27691..596819256a 100644 --- a/common/model.h +++ b/common/model.h @@ -1 +1 @@ -#define DEFAULT_MODEL "Vegan Filet O Fish (Default)" +#define DEFAULT_MODEL "Vegetarian Filet o Fish (Default)" diff --git a/common/params_keys.h b/common/params_keys.h index cb6f50cacd..02dd8fee85 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -71,7 +71,7 @@ inline static std::unordered_map keys = { {"LastPowerDropDetected", CLEAR_ON_MANAGER_START}, {"LastUpdateException", CLEAR_ON_MANAGER_START}, {"LastUpdateTime", PERSISTENT}, - {"LiveDelay", PERSISTENT}, + {"LiveDelay", PERSISTENT | BACKUP}, {"LiveParameters", PERSISTENT}, {"LiveParametersV2", PERSISTENT}, {"LiveTorqueParameters", PERSISTENT | DONT_LOG}, @@ -93,6 +93,7 @@ inline static std::unordered_map keys = { {"Offroad_TemperatureTooHigh", CLEAR_ON_MANAGER_START}, {"Offroad_UnofficialHardware", CLEAR_ON_MANAGER_START}, {"Offroad_UpdateFailed", CLEAR_ON_MANAGER_START}, + {"OnroadCycleRequested", CLEAR_ON_MANAGER_START}, {"OpenpilotEnabledToggle", PERSISTENT | BACKUP}, {"PandaHeartbeatLost", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, {"PandaSomResetTriggered", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, @@ -124,15 +125,21 @@ inline static std::unordered_map keys = { {"ApiCache_DriveStats", PERSISTENT}, {"AutoLaneChangeBsmDelay", PERSISTENT}, {"AutoLaneChangeTimer", PERSISTENT}, + {"BlinkerMinLateralControlSpeed", PERSISTENT | BACKUP}, + {"BlinkerPauseLateralControl", PERSISTENT | BACKUP}, {"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"CarParamsSPCache", CLEAR_ON_MANAGER_START}, {"CarParamsSPPersistent", PERSISTENT}, {"CarPlatformBundle", PERSISTENT}, + {"CustomAccIncrementsEnabled", PERSISTENT | BACKUP}, + {"CustomAccLongPressIncrement", PERSISTENT | BACKUP}, + {"CustomAccShortPressIncrement", PERSISTENT | BACKUP}, + {"DeviceBootMode", PERSISTENT | BACKUP}, {"EnableGithubRunner", PERSISTENT | BACKUP}, {"MaxTimeOffroad", PERSISTENT | BACKUP}, + {"Brightness", PERSISTENT | BACKUP}, {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, {"OffroadMode", CLEAR_ON_MANAGER_START}, - {"OffroadMode_Status", CLEAR_ON_MANAGER_START}, {"QuietMode", PERSISTENT | BACKUP}, // MADS params @@ -167,4 +174,28 @@ inline static std::unordered_map keys = { {"HyundaiLongitudinalTuning", PERSISTENT}, {"DynamicExperimentalControl", PERSISTENT}, + {"BlindSpot", PERSISTENT | BACKUP}, + + // model panel params + {"LagdToggle", PERSISTENT | BACKUP}, + + // mapd + {"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, + {"MapdVersion", PERSISTENT}, + {"MapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, + {"NextMapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, + {"Offroad_OSMUpdateRequired", CLEAR_ON_MANAGER_START}, + {"OsmDbUpdatesCheck", CLEAR_ON_MANAGER_START}, // mapd database update happens with device ON, reset on boot + {"OSMDownloadBounds", PERSISTENT}, + {"OsmDownloadedDate", PERSISTENT}, + {"OSMDownloadLocations", PERSISTENT}, + {"OSMDownloadProgress", CLEAR_ON_MANAGER_START}, + {"OsmLocal", PERSISTENT}, + {"OsmLocationName", PERSISTENT}, + {"OsmLocationTitle", PERSISTENT}, + {"OsmLocationUrl", PERSISTENT}, + {"OsmStateName", PERSISTENT}, + {"OsmStateTitle", PERSISTENT}, + {"OsmWayTest", PERSISTENT}, + {"RoadName", CLEAR_ON_ONROAD_TRANSITION}, }; diff --git a/common/spinner.py b/common/spinner.py new file mode 100755 index 0000000000..12a816eaf8 --- /dev/null +++ b/common/spinner.py @@ -0,0 +1,52 @@ +import os +import subprocess +from openpilot.common.basedir import BASEDIR + + +class Spinner: + def __init__(self): + try: + self.spinner_proc = subprocess.Popen(["./spinner.py"], + stdin=subprocess.PIPE, + cwd=os.path.join(BASEDIR, "system", "ui"), + close_fds=True) + except OSError: + self.spinner_proc = None + + def __enter__(self): + return self + + def update(self, spinner_text: str): + if self.spinner_proc is not None: + self.spinner_proc.stdin.write(spinner_text.encode('utf8') + b"\n") + try: + self.spinner_proc.stdin.flush() + except BrokenPipeError: + pass + + def update_progress(self, cur: float, total: float): + self.update(str(round(100 * cur / total))) + + def close(self): + if self.spinner_proc is not None: + self.spinner_proc.kill() + try: + self.spinner_proc.communicate(timeout=2.) + except subprocess.TimeoutExpired: + print("WARNING: failed to kill spinner") + self.spinner_proc = None + + def __del__(self): + self.close() + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +if __name__ == "__main__": + import time + with Spinner() as s: + s.update("Spinner text") + time.sleep(5.0) + print("gone") + time.sleep(5.0) diff --git a/common/text_window.py b/common/text_window.py new file mode 100755 index 0000000000..358243d1f1 --- /dev/null +++ b/common/text_window.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +import os +import time +import subprocess +from openpilot.common.basedir import BASEDIR + + +class TextWindow: + def __init__(self, text): + try: + self.text_proc = subprocess.Popen(["./text.py", text], + stdin=subprocess.PIPE, + cwd=os.path.join(BASEDIR, "system", "ui"), + close_fds=True) + except OSError: + self.text_proc = None + + def get_status(self): + if self.text_proc is not None: + self.text_proc.poll() + return self.text_proc.returncode + return None + + def __enter__(self): + return self + + def close(self): + if self.text_proc is not None: + self.text_proc.terminate() + self.text_proc = None + + def wait_for_exit(self): + if self.text_proc is not None: + while True: + if self.get_status() == 1: + return + time.sleep(0.1) + + def __del__(self): + self.close() + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +if __name__ == "__main__": + text = """Traceback (most recent call last): + File "./controlsd.py", line 608, in + main() + File "./controlsd.py", line 604, in main + controlsd_thread(sm, pm, logcan) + File "./controlsd.py", line 455, in controlsd_thread + 1/0 +ZeroDivisionError: division by zero""" + print(text) + + with TextWindow(text) as s: + for _ in range(100): + if s.get_status() == 1: + print("Got exit button") + break + time.sleep(0.1) + print("gone") diff --git a/common/util.py b/common/util.py index 33885a9024..e6ddb46e7b 100644 --- a/common/util.py +++ b/common/util.py @@ -1,3 +1,25 @@ +import os +import subprocess + +def sudo_write(val: str, path: str) -> None: + try: + with open(path, 'w') as f: + f.write(str(val)) + except PermissionError: + os.system(f"sudo chmod a+w {path}") + try: + with open(path, 'w') as f: + f.write(str(val)) + except PermissionError: + # fallback for debugfs files + os.system(f"sudo su -c 'echo {val} > {path}'") + +def sudo_read(path: str) -> str: + try: + return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() + except Exception: + return "" + class MovingAverage: def __init__(self, window_size: int): self.window_size: int = window_size diff --git a/common/version.h b/common/version.h index 6351a5b3ff..67b69da7c7 100644 --- a/common/version.h +++ b/common/version.h @@ -1 +1 @@ -#define COMMA_VERSION "0.9.9" +#define COMMA_VERSION "0.9.10" diff --git a/common/watchdog.py b/common/watchdog.py new file mode 100644 index 0000000000..ddb6f744e9 --- /dev/null +++ b/common/watchdog.py @@ -0,0 +1,22 @@ +import os +import time +import struct +from openpilot.system.hardware.hw import Paths + +WATCHDOG_FN = f"{Paths.shm_path()}/wd_" +_LAST_KICK = 0.0 + +def kick_watchdog(): + global _LAST_KICK + current_time = time.monotonic() + + if current_time - _LAST_KICK < 1.0: + return + + try: + with open(f"{WATCHDOG_FN}{os.getpid()}", 'wb') as f: + f.write(struct.pack('1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Audi|Q3 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -38,6 +38,7 @@ A supported vehicle is one that just works when you install a comma device. All |Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| |Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| +|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=MewJc9LYp9M| |Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=MewJc9LYp9M| @@ -53,7 +54,7 @@ A supported vehicle is one that just works when you install a comma device. All |Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 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
||| |Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 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
||| |Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 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
||| -|Ford|Mustang Mach-E 2021-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| +|Ford|Mustang Mach-E 2021-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| |Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=uUGkH6C_EQU| |Genesis|G70 2018|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Genesis|G70 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -77,8 +78,8 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B 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 Hatchback 2017-21|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B 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 Hatchback Hybrid 2023 (Europe only)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B 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 Hatchback Hybrid 2025|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B 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 Hatchback Hybrid (Europe only) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B 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|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.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|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -115,7 +116,6 @@ A supported vehicle is one that just works when you install a comma device. All |Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 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
||| |Hyundai|Ioniq Plug-in Hybrid 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
||| |Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona 2022|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 O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Hyundai|Kona Electric (with HDA II, Korea only) 2023[6](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai R connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -181,12 +181,12 @@ A supported vehicle is one that just works when you install a comma device. All |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
||| |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-24|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
||| +|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
||| |Lexus|ES Hybrid 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 Hybrid 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
||| |Lexus|GS F 2016|All|Stock|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|IS 2017-19|All|Stock|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|IS 2022-23|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
||| +|Lexus|IS 2022-24|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
||| |Lexus|LC 2024|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
||| |Lexus|NX 2018-19|All|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|NX 2020-21|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
||| @@ -313,11 +313,11 @@ A supported vehicle is one that just works when you install a comma device. All |Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| |Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
[17](#footnotes)||| |Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Taos 2022-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Tiguan 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d4a9561f2c..1950db8a05 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -7,7 +7,6 @@ Development is coordinated through [Discord](https://discord.comma.ai) and GitHu ### Getting Started * Setup your [development environment](../tools/) -* Read about the [development workflow](WORKFLOW.md) * Join our [Discord](https://discord.comma.ai) * Docs are at https://docs.comma.ai and https://blog.comma.ai diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md deleted file mode 100644 index 1c5bbe9a63..0000000000 --- a/docs/WORKFLOW.md +++ /dev/null @@ -1,33 +0,0 @@ -# openpilot development workflow - -Aside from the ML models, most tools used for openpilot development are in this repo. - -Most development happens on normal Ubuntu workstations, and not in cars or directly on comma devices. See the [setup guide](../tools) for getting your PC setup for openpilot development. - -## Quick start - -```bash -# get the latest stuff -git pull -git lfs pull -git submodule update --init --recursive - -# update dependencies -tools/ubuntu_setup.sh - -# build everything -scons -j$(nproc) - -# build just the ui with either of these -scons -j8 selfdrive/ui/ -cd selfdrive/ui/ && scons -u -j8 - -# test everything -pytest - -# test just logging services -cd system/loggerd && pytest . - -# run the linter -op lint -``` diff --git a/launch_env.sh b/launch_env.sh index 6f31fcf776..b0bff4e57f 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.2" + export AGNOS_VERSION="12.4" fi export STAGING_ROOT="/data/safe_staging" diff --git a/msgq_repo b/msgq_repo index a144e41d5f..fd7bd0df50 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit a144e41d5ff382177b979bafeab54cf8d39f6f3e +Subproject commit fd7bd0df50a95dca3f180705721aa1fa300aef0f diff --git a/opendbc_repo b/opendbc_repo index 32baa7eac5..5d0b6435c4 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 32baa7eac54f53acfe2e4d3a25735a28c297672a +Subproject commit 5d0b6435c4359bd60433e089eac41ebd4361db0e diff --git a/panda b/panda index 5a1b1c9225..c33cfa0803 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 5a1b1c9225206e9a4d11e06790f5a571de7fa674 +Subproject commit c33cfa0803deef5dc53d103ae1a2513e2ab94196 diff --git a/pyproject.toml b/pyproject.toml index 3287bc259a..68e5e35296 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,9 @@ dependencies = [ # logreader "zstandard", + + # ui + "qrcode", ] [project.optional-dependencies] @@ -85,7 +88,8 @@ testing = [ "pytest-cov", "pytest-cpp", "pytest-subtests", - "pytest-xdist", + # https://github.com/pytest-dev/pytest-xdist/issues/1215 + "pytest-xdist @ git+https://github.com/sshane/pytest-xdist@909e97b49d12401c10608f9d777bfc9dab8a4413", "pytest-timeout", "pytest-randomly", "pytest-asyncio", @@ -102,8 +106,8 @@ dev = [ "azure-storage-blob", "dbus-next", "dictdiffer", - "lru-dict", "matplotlib", + "opencv-python-headless", "parameterized >=0.8, <0.9", "pyautogui", "pygame", @@ -260,6 +264,7 @@ lint.flake8-implicit-str-concat.allow-multiline = false "tools".msg = "Use openpilot.tools" "pytest.main".msg = "pytest.main requires special handling that is easy to mess up!" "unittest".msg = "Use pytest" +"pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure" [tool.coverage.run] concurrency = ["multiprocessing", "thread"] diff --git a/rednose_repo b/rednose_repo index 8b86052919..7fddc8e6d4 160000 --- a/rednose_repo +++ b/rednose_repo @@ -1 +1 @@ -Subproject commit 8b860529199569401d5d8e105bc1be600fca49a8 +Subproject commit 7fddc8e6d49def83c952a78673179bdc62789214 diff --git a/release/README.md b/release/README.md index 1d3e784936..06404be6ab 100644 --- a/release/README.md +++ b/release/README.md @@ -1,36 +1,31 @@ # openpilot releases +``` ## release checklist **Go to `devel-staging`** +- [ ] update RELEASES.md - [ ] update `devel-staging`: `git reset --hard origin/master-ci` - [ ] open a pull request from `devel-staging` to `devel` +- [ ] post on Discord **Go to `devel`** -- [ ] update RELEASES.md -- [ ] close out milestone -- [ ] post on Discord dev channel - [ ] bump version on master: `common/version.h` and `RELEASES.md` -- [ ] merge the pull request - -tests: -- [ ] update from previous release -> new release -- [ ] update from new release -> previous release -- [ ] fresh install with `openpilot-test.comma.ai` -- [ ] drive on fresh install -- [ ] comma body test -- [ ] no submodules or LFS -- [ ] check sentry, MTBF, etc. +- [ ] before merging the pull request + - [ ] 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. **Go to `release3`** - [ ] publish the blog post - [ ] `git reset --hard origin/release3-staging` -- [ ] tag the release -``` -git tag v0.X.X -git push origin v0.X.X -``` +- [ ] tag the release: `git tag v0.X.X && git push origin v0.X.X` - [ ] create GitHub release - [ ] final test install on `openpilot.comma.ai` -- [ ] update production -- [ ] Post on Discord, X, etc. +- [ ] update factory provisioning +- [ ] close out milestone +- [ ] post on Discord, X, etc. +``` diff --git a/scripts/usb.sh b/scripts/usb.sh new file mode 100755 index 0000000000..5796cfa028 --- /dev/null +++ b/scripts/usb.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# testing the GPU box + +export XDG_CACHE_HOME=/data/tinycache +mkdir -p $XDG_CACHE_HOME + +cd /data/openpilot/tinygrad_repo/examples +while true; do + AMD=1 AMD_IFACE=usb python ./beautiful_cartpole.py + sleep 1 +done diff --git a/selfdrive/assets/offroad/fcc.html b/selfdrive/assets/offroad/fcc.html index 793bea533c..960a7a06cb 100644 --- a/selfdrive/assets/offroad/fcc.html +++ b/selfdrive/assets/offroad/fcc.html @@ -12,11 +12,10 @@
Quectel/EG25-G

FCC ID: XMR201903EG25G

-

- This device complies with Part 15 of the FCC Rules. - Operation is subject to the following two conditions: +

This device complies with Part 15 of the FCC Rules.

+

Operation is subject to the following two conditions:

-

(1) this device may not cause harmful interference, and +

(1) this device may not cause harmful interference, and

(2) this device must accept any interference received, including interference that may cause undesired operation.

The following test reports are subject to this declaration: diff --git a/selfdrive/car/car_specific.py b/selfdrive/car/car_specific.py index d441c648ff..151a8479a7 100644 --- a/selfdrive/car/car_specific.py +++ b/selfdrive/car/car_specific.py @@ -1,4 +1,4 @@ -from cereal import car, log +from cereal import car, log, custom import cereal.messaging as messaging from opendbc.car import DT_CTRL, structs from opendbc.car.interfaces import MAX_CTRL_SPEED @@ -16,14 +16,14 @@ class MockCarState: def __init__(self): self.sm = messaging.SubMaster(['gpsLocation', 'gpsLocationExternal']) - def update(self, CS: car.CarState): + def update(self, CS: car.CarState, CS_SP: custom.CarStateSP): self.sm.update(0) gps_sock = 'gpsLocationExternal' if self.sm.recv_frame['gpsLocationExternal'] > 1 else 'gpsLocation' CS.vEgo = self.sm[gps_sock].speed CS.vEgoRaw = self.sm[gps_sock].speed - return CS + return CS, CS_SP class CarSpecificEvents: diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index c9edc314a6..fc4c931334 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -73,12 +73,13 @@ class Car: def __init__(self, CI=None, RI=None) -> None: self.can_sock = messaging.sub_sock('can', timeout=20) self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents'] + ['carControlSP']) - self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks'] + ['carParamsSP']) + self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks'] + ['carParamsSP', 'carStateSP']) self.can_rcv_cum_timeout_counter = 0 self.CC_prev = car.CarControl.new_message() self.CS_prev = car.CarState.new_message() + self.CS_SP_prev = custom.CarStateSP.new_message() self.initialized_prev = False self.last_actuators_output = structs.CarControl.Actuators() @@ -87,6 +88,8 @@ class Car: self.can_callbacks = can_comm_callbacks(self.can_sock, self.pm.sock['sendcan']) + is_release = self.params.get_bool("IsReleaseBranch") + init_params_list_sp = sunnypilot_interfaces.get_init_params(self.params) if CI is None: @@ -108,7 +111,7 @@ class Car: fixed_fingerprint = json.loads(self.params.get("CarPlatformBundle", encoding='utf-8') or "{}").get("platform", None) - self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, num_pandas, cached_params, + self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, num_pandas, cached_params, fixed_fingerprint, init_params_list_sp) sunnypilot_interfaces.setup_interfaces(self.CI, self.params) self.RI = interfaces[self.CI.CP.carFingerprint].RadarInterface(self.CI.CP, self.CI.CP_SP) @@ -137,7 +140,7 @@ class Car: safety_config.safetyModel = structs.CarParams.SafetyModel.noOutput self.CP.safetyConfigs = [safety_config] - if self.CP.secOcRequired and not self.params.get_bool("IsReleaseBranch"): + if self.CP.secOcRequired and not is_release: # Copy user key if available try: with open("/cache/params/SecOCKey") as f: @@ -189,16 +192,17 @@ class Car: # log fingerprint in sentry sunnypilot_interfaces.log_fingerprint(self.CP) - def state_update(self) -> tuple[car.CarState, structs.RadarDataT | None]: + def state_update(self) -> tuple[car.CarState, custom.CarStateSP, structs.RadarDataT | None]: """carState update loop, driven by can""" can_strs = messaging.drain_sock_raw(self.can_sock, wait_for_one=True) can_list = can_capnp_to_list(can_strs) # Update carState from CAN - CS = self.CI.update(can_list) + CS, CS_SP = self.CI.update(can_list) + CS_SP = convert_to_capnp(CS_SP) if self.CP.brand == 'mock': - CS = self.mock_carstate.update(CS) + CS, CS_SP = self.mock_carstate.update(CS, CS_SP) # Update radar tracks from CAN RD: structs.RadarDataT | None = self.RI.update(can_list) @@ -223,9 +227,9 @@ class Car: CS.vCruise = float(self.v_cruise_helper.v_cruise_kph) CS.vCruiseCluster = float(self.v_cruise_helper.v_cruise_cluster_kph) - return CS, RD + return CS, CS_SP, RD - def state_publish(self, CS: car.CarState, RD: structs.RadarDataT | None): + def state_publish(self, CS: car.CarState, CS_SP: custom.CarStateSP, RD: structs.RadarDataT | None): """carState and carParams publish loop""" # carParams - logged every 50 seconds (> 1 per segment) @@ -262,6 +266,11 @@ class Car: cp_sp_send.carParamsSP = self.CP_SP_capnp self.pm.send('carParamsSP', cp_sp_send) + cs_sp_send = messaging.new_message('carStateSP') + cs_sp_send.valid = CS.canValid + cs_sp_send.carStateSP = CS_SP + self.pm.send('carStateSP', cs_sp_send) + def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.CarControlSP): """control update loop, driven by carControl""" @@ -281,9 +290,9 @@ class Car: self.CC_prev = CC def step(self): - CS, RD = self.state_update() + CS, CS_SP, RD = self.state_update() - self.state_publish(CS, RD) + self.state_publish(CS, CS_SP, RD) initialized = (not any(e.name == EventName.selfdriveInitializing for e in self.sm['onroadEvents']) and self.sm.seen['onroadEvents']) @@ -292,6 +301,7 @@ class Car: self.initialized_prev = initialized self.CS_prev = CS + self.CS_SP_prev = CS_SP def params_thread(self, evt): while not evt.is_set(): @@ -300,6 +310,7 @@ class Car: # sunnypilot self.dynamic_experimental_control = self.params.get_bool("DynamicExperimentalControl") + self.v_cruise_helper.read_custom_set_speed_params() time.sleep(0.1) diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index d696566ba5..01ef2d7c99 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -3,6 +3,7 @@ import numpy as np from cereal import car from openpilot.common.conversions import Conversions as CV +from openpilot.sunnypilot.selfdrive.car.cruise_ext import VCruiseHelperSP # WARNING: this value was determined based on the model's training distribution, @@ -28,8 +29,9 @@ CRUISE_INTERVAL_SIGN = { } -class VCruiseHelper: +class VCruiseHelper(VCruiseHelperSP): def __init__(self, CP): + VCruiseHelperSP.__init__(self) self.CP = CP self.v_cruise_kph = V_CRUISE_UNSET self.v_cruise_cluster_kph = V_CRUISE_UNSET @@ -99,7 +101,7 @@ class VCruiseHelper: if not self.button_change_states[button_type]["enabled"]: return - v_cruise_delta = v_cruise_delta * (5 if long_press else 1) + long_press, v_cruise_delta = VCruiseHelperSP.update_v_cruise_delta(self, long_press, v_cruise_delta) if long_press and self.v_cruise_kph % v_cruise_delta != 0: # partial interval self.v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](self.v_cruise_kph / v_cruise_delta) * v_cruise_delta else: diff --git a/selfdrive/car/helpers.py b/selfdrive/car/helpers.py index 6ebacfc976..6187be1474 100644 --- a/selfdrive/car/helpers.py +++ b/selfdrive/car/helpers.py @@ -35,11 +35,13 @@ def asdictref(obj) -> dict[str, Any]: return _asdictref_inner(obj) -def convert_to_capnp(struct: structs.CarParamsSP) -> capnp.lib.capnp._DynamicStructBuilder: +def convert_to_capnp(struct: structs.CarParamsSP | structs.CarStateSP) -> capnp.lib.capnp._DynamicStructBuilder: struct_dict = asdictref(struct) if isinstance(struct, structs.CarParamsSP): struct_capnp = custom.CarParamsSP.new_message(**struct_dict) + elif isinstance(struct, structs.CarStateSP): + struct_capnp = custom.CarStateSP.new_message(**struct_dict) else: raise ValueError(f"Unsupported struct type: {type(struct)}") diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 75e5467b7e..7d136b824c 100644 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -42,7 +42,7 @@ class TestCarInterfaces: args = get_fuzzy_car_interface_args(data.draw) car_params = CarInterface.get_params(car_name, args['fingerprints'], args['car_fw'], - alpha_long=args['alpha_long'], docs=False) + alpha_long=args['alpha_long'], is_release=False, docs=False) car_params_sp = CarInterface.get_params_sp(car_params, car_name, args['fingerprints'], args['car_fw'], alpha_long=args['alpha_long'], docs=False) car_params = car_params.as_reader() diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index ae5f57db9d..0fc6716dd3 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -153,7 +153,7 @@ class TestCarModelBase(unittest.TestCase): cls.openpilot_enabled = cls.car_safety_mode_frame is not None cls.CarInterface = interfaces[cls.platform] - cls.CP = cls.CarInterface.get_params(cls.platform, cls.fingerprint, car_fw, alpha_long, docs=False) + cls.CP = cls.CarInterface.get_params(cls.platform, cls.fingerprint, car_fw, alpha_long, False, docs=False) cls.CP_SP = cls.CarInterface.get_params_sp(cls.CP, cls.platform, cls.fingerprint, car_fw, alpha_long, docs=False) assert cls.CP assert cls.CP_SP @@ -204,7 +204,7 @@ class TestCarModelBase(unittest.TestCase): CC_SP = structs.CarControlSP() for i, msg in enumerate(self.can_msgs): - CS = self.CI.update(msg) + CS, _ = self.CI.update(msg) self.CI.apply(CC, CC_SP, msg[0]) if CS.canValid: @@ -349,7 +349,7 @@ class TestCarModelBase(unittest.TestCase): self.safety.safety_rx_hook(to_send) can = [(int(time.monotonic() * 1e9), [CanData(address=address, dat=dat, src=bus)])] - CS = self.CI.update(can) + CS, _ = self.CI.update(can) if self.safety.get_gas_pressed_prev() != prev_panda_gas: self.assertEqual(CS.gasPressed, self.safety.get_gas_pressed_prev()) @@ -409,7 +409,8 @@ class TestCarModelBase(unittest.TestCase): checks = defaultdict(int) vehicle_speed_seen = self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar for idx, can in enumerate(self.can_msgs): - CS = self.CI.update(can).as_reader() + CS, _ = self.CI.update(can) + CS = CS.as_reader() for msg in filter(lambda m: m.src < 64, can[1]): to_send = libsafety_py.make_CANPacket(msg.address, msg.src % 4, msg.dat) ret = self.safety.safety_rx_hook(to_send) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 576e73b7e6..04d79d3330 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -44,7 +44,7 @@ class Controls(ControlsExt): self.sm = messaging.SubMaster(['liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState', 'liveCalibration', 'livePose', 'longitudinalPlan', 'carState', 'carOutput', - 'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + self.sm_services_ext, + 'driverMonitoringState', 'onroadEvents', 'driverAssistance', 'liveDelay'] + self.sm_services_ext, poll='selfdriveState') self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext) @@ -93,6 +93,7 @@ class Controls(ControlsExt): torque_params.frictionCoefficientFiltered) self.LaC.extension.update_model_v2(self.sm['modelV2']) + self.LaC.extension.update_lateral_lag(self.sm['liveDelay'].lateralDelay) long_plan = self.sm['longitudinalPlan'] model_v2 = self.sm['modelV2'] diff --git a/selfdrive/debug/format_fingerprints.py b/selfdrive/debug/format_fingerprints.py deleted file mode 100755 index 7207bd1ef1..0000000000 --- a/selfdrive/debug/format_fingerprints.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -import jinja2 -import os - -from cereal import car -from openpilot.common.basedir import BASEDIR -from opendbc.car.interfaces import get_interface_attr - -Ecu = car.CarParams.Ecu - -CARS = get_interface_attr('CAR') -FW_VERSIONS = get_interface_attr('FW_VERSIONS') -FINGERPRINTS = get_interface_attr('FINGERPRINTS') -ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()} - -FINGERPRINTS_PY_TEMPLATE = jinja2.Template(""" -{%- if FINGERPRINTS[brand] %} -# ruff: noqa: E501 -{% endif %} -{% if FW_VERSIONS[brand] %} -from opendbc.car.structs import CarParams -{% endif %} -from opendbc.car.{{brand}}.values import CAR -{% if FW_VERSIONS[brand] %} - -Ecu = CarParams.Ecu -{% endif %} -{% if comments +%} -{{ comments | join() }} -{% endif %} -{% if FINGERPRINTS[brand] %} - -FINGERPRINTS = { -{% for car, fingerprints in FINGERPRINTS[brand].items() %} - CAR.{{car.name}}: [{ -{% for fingerprint in fingerprints %} -{% if not loop.first %} - {{ "{" }} -{% endif %} - {% for key, value in fingerprint.items() %}{{key}}: {{value}}{% if not loop.last %}, {% endif %}{% endfor %} - - }{% if loop.last %}]{% endif %}, -{% endfor %} -{% endfor %} -} -{% endif %} - -FW_VERSIONS{% if not FW_VERSIONS[brand] %}: dict[str, dict[tuple, list[bytes]]]{% endif %} = { -{% for car, _ in FW_VERSIONS[brand].items() %} - CAR.{{car.name}}: { -{% for key, fw_versions in FW_VERSIONS[brand][car].items() %} - (Ecu.{{ECU_NAME[key[0]]}}, 0x{{"%0x" | format(key[1] | int)}}, \ -{% if key[2] %}0x{{"%0x" | format(key[2] | int)}}{% else %}{{key[2]}}{% endif %}): [ - {% for fw_version in (fw_versions + extra_fw_versions.get(car, {}).get(key, [])) | unique | sort %} - {{fw_version}}, - {% endfor %} - ], -{% endfor %} - }, -{% endfor %} -} - -""", trim_blocks=True) - - -def format_brand_fw_versions(brand, extra_fw_versions: None | dict[str, dict[tuple, list[bytes]]] = None): - extra_fw_versions = extra_fw_versions or {} - - fingerprints_file = os.path.join(BASEDIR, f"opendbc/car/{brand}/fingerprints.py") - with open(fingerprints_file) as f: - comments = [line for line in f.readlines() if line.startswith("#") and "noqa" not in line] - - with open(fingerprints_file, "w") as f: - f.write(FINGERPRINTS_PY_TEMPLATE.render(brand=brand, comments=comments, ECU_NAME=ECU_NAME, - FINGERPRINTS=FINGERPRINTS, FW_VERSIONS=FW_VERSIONS, - extra_fw_versions=extra_fw_versions)) - - -if __name__ == "__main__": - for brand in FW_VERSIONS.keys(): - format_brand_fw_versions(brand) diff --git a/selfdrive/locationd/calibrationd.py b/selfdrive/locationd/calibrationd.py index e265b70f1a..59f30dbf77 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/selfdrive/locationd/calibrationd.py @@ -13,6 +13,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.params import Params from openpilot.common.realtime import config_realtime_process @@ -36,8 +37,11 @@ RPY_INIT = np.array([0.0,0.0,0.0]) WIDE_FROM_DEVICE_EULER_INIT = np.array([0.0, 0.0, 0.0]) HEIGHT_INIT = np.array([1.22]) -# These values are needed to accommodate the model frame in the narrow cam of the C3 -PITCH_LIMITS = np.array([-0.09074112085129739, 0.17]) +# These values are needed to accommodate the model frame in the narrow cam +if HARDWARE.get_device_type() == 'mici': + PITCH_LIMITS = np.array([-0.143101, 0.22235988]) +else: + PITCH_LIMITS = np.array([-0.09074112085129739, 0.17]) YAW_LIMITS = np.array([-0.06912048084718224, 0.06912048084718235]) DEBUG = os.getenv("DEBUG") is not None diff --git a/selfdrive/locationd/helpers.py b/selfdrive/locationd/helpers.py index a3e3ae4a8c..bf4588a40c 100644 --- a/selfdrive/locationd/helpers.py +++ b/selfdrive/locationd/helpers.py @@ -82,6 +82,12 @@ class PointBuckets: total_points_valid = self.__len__() >= self.min_points_total return individual_buckets_valid and total_points_valid + def get_valid_percent(self) -> int: + total_points_perc = min(self.__len__() / self.min_points_total * 100, 100) + individual_buckets_perc = min(min(len(v) / min_pts * 100 for v, min_pts in + zip(self.buckets.values(), self.buckets_min_points.values(), strict=True)), 100) + return int((total_points_perc + individual_buckets_perc) / 2) + def is_calculable(self) -> bool: return all(len(v) > 0 for v in self.buckets.values()) diff --git a/selfdrive/locationd/lagd.py b/selfdrive/locationd/lagd.py index 4e207b4882..e6767db473 100755 --- a/selfdrive/locationd/lagd.py +++ b/selfdrive/locationd/lagd.py @@ -157,7 +157,8 @@ class LateralLagEstimator: block_count: int = BLOCK_NUM, min_valid_block_count: int = BLOCK_NUM_NEEDED, block_size: int = BLOCK_SIZE, window_sec: float = MOVING_WINDOW_SEC, okay_window_sec: float = MIN_OKAY_WINDOW_SEC, min_recovery_buffer_sec: float = MIN_RECOVERY_BUFFER_SEC, min_vego: float = MIN_VEGO, min_yr: float = MIN_ABS_YAW_RATE, min_ncc: float = MIN_NCC, - max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE): + max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE, + enabled: bool = True): self.dt = dt self.window_sec = window_sec self.okay_window_sec = okay_window_sec @@ -172,6 +173,7 @@ class LateralLagEstimator: self.min_confidence = min_confidence self.max_lat_accel = max_lat_accel self.max_lat_accel_diff = max_lat_accel_diff + self.enabled = enabled self.t = 0.0 self.lat_active = False @@ -206,7 +208,7 @@ class LateralLagEstimator: liveDelay = msg.liveDelay valid_mean_lag, valid_std, current_mean_lag, current_std = self.block_avg.get() - if self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std): + if self.enabled and self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std): if valid_std > MAX_LAG_STD: liveDelay.status = log.LiveDelayData.Status.invalid else: @@ -227,6 +229,8 @@ class LateralLagEstimator: liveDelay.lateralDelayEstimateStd = 0.0 liveDelay.validBlocks = self.block_avg.valid_blocks + liveDelay.calPerc = min(100 * (self.block_avg.valid_blocks * self.block_size + self.block_avg.idx) // + (self.min_valid_block_count * self.block_size), 100) if debug: liveDelay.points = self.block_avg.values.flatten().tolist() @@ -303,7 +307,8 @@ class LateralLagEstimator: self.block_avg.update(delay) self.last_estimate_t = self.t - def actuator_delay(self, expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray, dt: float, max_lag: float) -> tuple[float, float, float]: + @staticmethod + def actuator_delay(expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray, dt: float, max_lag: float) -> tuple[float, float, float]: assert len(expected_sig) == len(actual_sig) max_lag_samples = int(max_lag / dt) padded_size = fft_next_good_size(len(expected_sig) + max_lag_samples) @@ -366,7 +371,10 @@ def main(): params = Params() CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) - lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency) + # TODO: remove me, lagd is in shadow mode on release + is_release = params.get_bool("IsReleaseBranch") + + lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency, enabled=not is_release) if (initial_lag_params := retrieve_initial_lag(params, CP)) is not None: lag, valid_blocks = initial_lag_params lag_learner.reset(lag, valid_blocks) diff --git a/selfdrive/locationd/test/test_lagd.py b/selfdrive/locationd/test/test_lagd.py index b805f1759d..8fb829edd8 100644 --- a/selfdrive/locationd/test/test_lagd.py +++ b/selfdrive/locationd/test/test_lagd.py @@ -3,7 +3,7 @@ import numpy as np import time import pytest -from cereal import messaging, log +from cereal import messaging, log, car from openpilot.selfdrive.locationd.lagd import LateralLagEstimator, retrieve_initial_lag, masked_normalized_cross_correlation, \ BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams @@ -16,11 +16,7 @@ MAX_ERR_FRAMES = 1 DT = 0.05 -def process_messages(mocker, estimator, lag_frames, n_frames, vego=20.0, rejection_threshold=0.0): - class ZeroMock(mocker.Mock): - def __getattr__(self, *args): - return 0 - +def process_messages(estimator, lag_frames, n_frames, vego=20.0, rejection_threshold=0.0): for i in range(n_frames): t = i * estimator.dt desired_la = np.cos(10 * t) * 0.1 @@ -31,19 +27,15 @@ def process_messages(mocker, estimator, lag_frames, n_frames, vego=20.0, rejecti if rejected: actual_la = desired_la - desired_cuvature = desired_la / (vego ** 2) - actual_yr = actual_la / vego + desired_cuvature = float(desired_la / (vego ** 2)) + actual_yr = float(actual_la / vego) msgs = [ - (t, "carControl", mocker.Mock(latActive=not rejected)), - (t, "carState", mocker.Mock(vEgo=vego, steeringPressed=False)), - (t, "controlsState", mocker.Mock(desiredCurvature=desired_cuvature, - lateralControlState=mocker.Mock(which=mocker.Mock(return_value='debugControlState'), debugControlState=ZeroMock()))), - (t, "livePose", mocker.Mock(orientationNED=ZeroMock(), - velocityDevice=ZeroMock(), - accelerationDevice=ZeroMock(), - angularVelocityDevice=ZeroMock(z=actual_yr, valid=True), - posenetOK=True, inputsOK=True)), - (t, "liveCalibration", mocker.Mock(rpyCalib=[0, 0, 0], calStatus=log.LiveCalibrationData.Status.calibrated)), + (t, "carControl", car.CarControl(latActive=not rejected)), + (t, "carState", car.CarState(vEgo=vego, steeringPressed=False)), + (t, "controlsState", log.ControlsState(desiredCurvature=desired_cuvature)), + (t, "livePose", log.LivePose(angularVelocityDevice=log.LivePose.XYZMeasurement(z=actual_yr, valid=True), + posenetOK=True, inputsOK=True)), + (t, "liveCalibration", log.LiveCalibrationData(rpyCalib=[0, 0, 0], calStatus=log.LiveCalibrationData.Status.calibrated)), ] for t, w, m in msgs: estimator.handle_log(t, w, m) @@ -94,40 +86,56 @@ class TestLagd: corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20] assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1) - def test_empty_estimator(self, mocker): - mocked_CP = mocker.Mock(steerActuatorDelay=0.8) + def test_empty_estimator(self): + mocked_CP = car.CarParams(steerActuatorDelay=0.8) estimator = LateralLagEstimator(mocked_CP, DT) msg = estimator.get_msg(True) assert msg.liveDelay.status == 'unestimated' assert np.allclose(msg.liveDelay.lateralDelay, estimator.initial_lag) assert np.allclose(msg.liveDelay.lateralDelayEstimate, estimator.initial_lag) assert msg.liveDelay.validBlocks == 0 + assert msg.liveDelay.calPerc == 0 - def test_estimator_basics(self, mocker, subtests): + def test_estimator_basics(self, subtests): for lag_frames in range(5): with subtests.test(msg=f"lag_frames={lag_frames}"): - mocked_CP = mocker.Mock(steerActuatorDelay=0.8) + mocked_CP = car.CarParams(steerActuatorDelay=0.8) estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0) - process_messages(mocker, estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE) + process_messages(estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE) msg = estimator.get_msg(True) assert msg.liveDelay.status == 'estimated' assert np.allclose(msg.liveDelay.lateralDelay, lag_frames * DT, atol=0.01) assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01) assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01) assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED + assert msg.liveDelay.calPerc == 100 - def test_estimator_masking(self, mocker): - mocked_CP, lag_frames = mocker.Mock(steerActuatorDelay=0.8), random.randint(1, 19) + def test_disabled_estimator(self): + mocked_CP = car.CarParams(steerActuatorDelay=0.8) + estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, enabled=False) + lag_frames = 5 + process_messages(estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE) + msg = estimator.get_msg(True) + assert msg.liveDelay.status == 'unestimated' + assert np.allclose(msg.liveDelay.lateralDelay, 1.0, atol=0.01) + assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01) + assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01) + assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED + assert msg.liveDelay.calPerc == 100 + + def test_estimator_masking(self): + mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19) estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1) - process_messages(mocker, estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4) + process_messages(estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4) msg = estimator.get_msg(True) assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01) assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01) + assert msg.liveDelay.calPerc == 100 @pytest.mark.skipif(PC, reason="only on device") @pytest.mark.timeout(60) - def test_estimator_performance(self, mocker): - mocked_CP = mocker.Mock(steerActuatorDelay=0.8) + def test_estimator_performance(self): + mocked_CP = car.CarParams(steerActuatorDelay=0.8) estimator = LateralLagEstimator(mocked_CP, DT) ds = [] diff --git a/selfdrive/locationd/test/test_torqued.py b/selfdrive/locationd/test/test_torqued.py new file mode 100644 index 0000000000..53f3120c36 --- /dev/null +++ b/selfdrive/locationd/test/test_torqued.py @@ -0,0 +1,25 @@ +from cereal import car +from openpilot.selfdrive.locationd.torqued import TorqueEstimator + + +def test_cal_percent(): + est = TorqueEstimator(car.CarParams()) + msg = est.get_msg() + assert msg.liveTorqueParameters.calPerc == 0 + + for (low, high), min_pts in zip(est.filtered_points.buckets.keys(), + est.filtered_points.buckets_min_points.values(), strict=True): + for _ in range(int(min_pts)): + est.filtered_points.add_point((low + high) / 2.0, 0.0) + + # enough bucket points, but not enough total points + msg = est.get_msg() + assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2 + + # add enough points to bucket with most capacity + key = list(est.filtered_points.buckets)[0] + for _ in range(est.min_points_total - len(est.filtered_points)): + est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0) + + msg = est.get_msg() + assert msg.liveTorqueParameters.calPerc == 100 diff --git a/selfdrive/locationd/torqued.py b/selfdrive/locationd/torqued.py index 5ed86e457f..23bd99931b 100755 --- a/selfdrive/locationd/torqued.py +++ b/selfdrive/locationd/torqued.py @@ -233,6 +233,7 @@ class TorqueEstimator(ParameterEstimator): liveTorqueParameters.latAccelOffsetFiltered = float(self.filtered_params['latAccelOffset'].x) liveTorqueParameters.frictionCoefficientFiltered = float(self.filtered_params['frictionCoefficient'].x) liveTorqueParameters.totalBucketPoints = len(self.filtered_points) + liveTorqueParameters.calPerc = self.filtered_points.get_valid_percent() liveTorqueParameters.decay = self.decay liveTorqueParameters.maxResets = self.resets return msg diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 10f3876190..545fcdcff7 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -60,7 +60,7 @@ import subprocess from tinygrad import Device # because tg doesn't support multi-process -devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True) +devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True, cwd=env.Dir('#').abspath) if b"AMD" in devs: del Device print("USB GPU detected... building") diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 1e3d1782e6..ac88dfbb5f 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -86,10 +86,20 @@ class ModelState: prev_desire: np.ndarray # for tracking the rising edge of the pulse def __init__(self, context: CLContext): - self.frames = { - 'input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP), - 'big_input_imgs': DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP) - } + with open(VISION_METADATA_PATH, 'rb') as f: + vision_metadata = pickle.load(f) + self.vision_input_shapes = vision_metadata['input_shapes'] + self.vision_input_names = list(self.vision_input_shapes.keys()) + self.vision_output_slices = vision_metadata['output_slices'] + vision_output_size = vision_metadata['output_shapes']['outputs'][1] + + with open(POLICY_METADATA_PATH, 'rb') as f: + policy_metadata = pickle.load(f) + self.policy_input_shapes = policy_metadata['input_shapes'] + self.policy_output_slices = policy_metadata['output_slices'] + policy_output_size = policy_metadata['output_shapes']['outputs'][1] + + self.frames = {name: DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP) for name in self.vision_input_names} self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) self.full_features_buffer = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32) @@ -106,18 +116,6 @@ class ModelState: 'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32), } - with open(VISION_METADATA_PATH, 'rb') as f: - vision_metadata = pickle.load(f) - self.vision_input_shapes = vision_metadata['input_shapes'] - self.vision_output_slices = vision_metadata['output_slices'] - vision_output_size = vision_metadata['output_shapes']['outputs'][1] - - with open(POLICY_METADATA_PATH, 'rb') as f: - policy_metadata = pickle.load(f) - self.policy_input_shapes = policy_metadata['input_shapes'] - self.policy_output_slices = policy_metadata['output_slices'] - policy_output_size = policy_metadata['output_shapes']['outputs'][1] - # img buffers are managed in openCL transform code self.vision_inputs: dict[str, Tensor] = {} self.vision_output = np.zeros(vision_output_size, dtype=np.float32) @@ -135,7 +133,7 @@ class ModelState: parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()} return parsed_model_outputs - def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, + def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: # Model decides when action is completed, so desire input is just a pulse triggered on rising edge inputs['desire'][0] = 0 @@ -148,8 +146,7 @@ class ModelState: self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention'] self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params'] - imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()), - 'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())} + imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names} if TICI and not USBGPU: # The imgs tensors are backed by opencl memory, only need init once @@ -328,14 +325,16 @@ def main(demo=False): if prepare_only: cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames") + bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names} + transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names} inputs:dict[str, np.ndarray] = { 'desire': vec_desire, 'traffic_convention': traffic_convention, 'lateral_control_params': lateral_control_params, - } + } mt1 = time.perf_counter() - model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only) + model_output = model.run(bufs, transforms, inputs, prepare_only) mt2 = time.perf_counter() model_execution_time = mt2 - mt1 diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index 5da6d85300..343c3a0aa8 100644 --- a/selfdrive/modeld/models/driving_policy.onnx +++ b/selfdrive/modeld/models/driving_policy.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abbb1f5a74a6d047ed4b7f376b19451a9443986660f136afd0f8d76fc254a579 -size 15976894 +oid sha256:5f714fb38bcd44b5d9f44e3a8e1595e1dfdf7558f0eec3485cf3f2dbb6dc7d8d +size 15971805 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index 6c8cf592b0..924d11304d 100644 --- a/selfdrive/modeld/models/driving_vision.onnx +++ b/selfdrive/modeld/models/driving_vision.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dfffac033d7ae8e68aa5763bd9b5dd99ddc748f6a95523c84fc2523eefdec55 -size 34882971 +oid sha256:3ac4867fbc618037e8d03143edbfeeae960f2025644b5dcf36c6665271b4f874 +size 34883375 diff --git a/selfdrive/selfdrived/alerts_offroad.json b/selfdrive/selfdrived/alerts_offroad.json index 68f5949398..1668bf17fd 100644 --- a/selfdrive/selfdrived/alerts_offroad.json +++ b/selfdrive/selfdrived/alerts_offroad.json @@ -45,8 +45,8 @@ "text": "sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.", "severity": 0 }, - "OffroadMode_Status": { - "text": "sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to \"Settings\" -> \"Device\" to exit Always Offroad mode.", - "severity": 1 + "Offroad_OSMUpdateRequired": { + "text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1", + "severity": 0 } } diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 695111bd58..9bfc638db3 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -192,6 +192,16 @@ def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging return NormalPermanentAlert(f"Driving Personality: {personality}", duration=1.5) +def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + text = "Toggle stock LKAS on or off to engage" + if CP.brand == "tesla": + text = "Switch to Traffic-Aware Cruise Control to engage" + elif CP.brand == "mazda": + text = "Enable your car's LKAS to engage" + elif CP.brand == "nissan": + text = "Disable your car's stock LKAS to engage" + return NormalPermanentAlert("Invalid LKAS setting", text) + EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { # ********** events with no alerts ********** @@ -245,8 +255,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { }, EventName.invalidLkasSetting: { - ET.PERMANENT: NormalPermanentAlert("Invalid LKAS setting", - "Toggle stock LKAS on or off to engage"), + ET.PERMANENT: invalid_lkas_setting_alert, ET.NO_ENTRY: NoEntryAlert("Invalid LKAS setting"), }, @@ -576,11 +585,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { }, EventName.noGps: { - ET.PERMANENT: Alert( - "Poor GPS reception", - "Ensure device has a clear view of the sky", - AlertStatus.normal, AlertSize.mid, - Priority.LOWER, VisualAlert.none, AudibleAlert.none, .2, creation_delay=600.) }, EventName.tooDistracted: { diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index e32d7dfb23..dfda2c31f7 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -129,8 +129,10 @@ class SelfdriveD(CruiseHelper): # some comma three with NVMe experience NVMe dropouts mid-drive that # cause loggerd to crash on write, so ignore it only on that platform self.ignored_processes = set() - if HARDWARE.get_device_type() == 'tici' and os.path.exists('/dev/nvme0'): + nvme_expected = os.path.exists('/dev/nvme0n1') or (not os.path.isfile("/persist/comma/living-in-the-moment")) + if HARDWARE.get_device_type() == 'tici' and nvme_expected: self.ignored_processes = {'loggerd', } + self.ignored_processes.update({'mapd'}) # Determine startup event self.startup_event = EventName.startup if build_metadata.openpilot.comma_remote and build_metadata.tested_channel else EventName.startupMaster @@ -380,16 +382,16 @@ class SelfdriveD(CruiseHelper): if (planner_fcw or model_fcw) and not self.CP.notCar: self.events.add(EventName.fcw) + # GPS checks + gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0 + if not gps_ok and self.sm['livePose'].inputsOK and (self.distance_traveled > 1500): + self.events.add(EventName.noGps) + if gps_ok: + self.distance_traveled = 0 + self.distance_traveled += abs(CS.vEgo) * DT_CTRL + # TODO: fix simulator if not SIMULATION or REPLAY: - # Not show in first 1.5 km to allow for driving out of garage. This event shows after 5 minutes - gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0 - if not gps_ok and self.sm['livePose'].inputsOK and (self.distance_traveled > 1500): - self.events.add(EventName.noGps) - if gps_ok: - self.distance_traveled = 0 - self.distance_traveled += abs(CS.vEgo) * DT_CTRL - if self.sm['modelV2'].frameDropPerc > 20: self.events.add(EventName.modeldLagging) @@ -544,6 +546,7 @@ class SelfdriveD(CruiseHelper): def params_thread(self, evt): while not evt.is_set(): self.is_metric = self.params.get_bool("IsMetric") + self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled") self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator") self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl self.personality = self.read_personality_param() diff --git a/selfdrive/test/ci_shell.sh b/selfdrive/test/ci_shell.sh deleted file mode 100755 index 76f0a9f78c..0000000000 --- a/selfdrive/test/ci_shell.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -e - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -OP_ROOT="$DIR/../../" - -if [ -z "$BUILD" ]; then - docker pull ghcr.io/commaai/openpilot-base:latest -else - docker build --cache-from ghcr.io/commaai/openpilot-base:latest -t ghcr.io/commaai/openpilot-base:latest -f $OP_ROOT/Dockerfile.openpilot_base . -fi - -docker run \ - -it \ - --rm \ - --volume $OP_ROOT:$OP_ROOT \ - --workdir $PWD \ - --env PYTHONPATH=$OP_ROOT \ - ghcr.io/commaai/openpilot-base:latest \ - /bin/bash diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index 0578a61588..59b8cf8250 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import os +import pickle import sys from collections import defaultdict from typing import Any @@ -15,15 +16,15 @@ from openpilot.system.hardware import PC from openpilot.tools.lib.openpilotci import get_url from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff from openpilot.selfdrive.test.process_replay.process_replay import get_process_config, replay_process -from openpilot.tools.lib.framereader import FrameReader, NumpyFrameReader +from openpilot.tools.lib.framereader import FrameReader from openpilot.tools.lib.logreader import LogReader, save_log from openpilot.tools.lib.github_utils import GithubUtils TEST_ROUTE = "8494c69d3c710e81|000001d4--2648a9a404" SEGMENT = 4 -MAX_FRAMES = 100 if PC else 400 +START_FRAME = 0 +END_FRAME = 60 -NO_MODEL = "NO_MODEL" in os.environ SEND_EXTRA_INPUTS = bool(int(os.getenv("SEND_EXTRA_INPUTS", "0"))) DATA_TOKEN = os.getenv("CI_ARTIFACTS_TOKEN","") @@ -125,16 +126,15 @@ def comment_replay_report(proposed, master, full_logs): comment = f"ref for commit {commit}: {link}/{log_name}" + diff_plots + all_plots GITHUB.comment_on_pr(comment, PR_BRANCH, "commaci-public", True) -def trim_logs_to_max_frames(logs, max_frames, frs_types, include_all_types): +def trim_logs(logs, start_frame, end_frame, frs_types, include_all_types): all_msgs = [] cam_state_counts = defaultdict(int) - # keep adding messages until cam states are equal to MAX_FRAMES for msg in sorted(logs, key=lambda m: m.logMonoTime): - all_msgs.append(msg) if msg.which() in frs_types: cam_state_counts[msg.which()] += 1 - - if all(cam_state_counts[state] == max_frames for state in frs_types): + if any(cam_state_counts[state] >= start_frame for state in frs_types): + all_msgs.append(msg) + if all(cam_state_counts[state] == end_frame for state in frs_types): break if len(include_all_types) != 0: @@ -146,9 +146,9 @@ def trim_logs_to_max_frames(logs, max_frames, frs_types, include_all_types): def model_replay(lr, frs): # modeld is using frame pairs - modeld_logs = trim_logs_to_max_frames(lr, MAX_FRAMES, {"roadCameraState", "wideRoadCameraState"}, - {"roadEncodeIdx", "wideRoadEncodeIdx", "carParams", "carState", "carControl"}) - dmodeld_logs = trim_logs_to_max_frames(lr, MAX_FRAMES, {"driverCameraState"}, {"driverEncodeIdx", "carParams"}) + modeld_logs = trim_logs(lr, START_FRAME, END_FRAME, {"roadCameraState", "wideRoadCameraState"}, + {"roadEncodeIdx", "wideRoadEncodeIdx", "carParams", "carState", "carControl", "can"}) + dmodeld_logs = trim_logs(lr, START_FRAME, END_FRAME, {"driverCameraState"}, {"driverEncodeIdx", "carParams", "can"}) if not SEND_EXTRA_INPUTS: modeld_logs = [msg for msg in modeld_logs if msg.which() != 'liveCalibration'] @@ -165,9 +165,6 @@ def model_replay(lr, frs): dmonitoringmodeld = get_process_config("dmonitoringmodeld") modeld_msgs = replay_process(modeld, modeld_logs, frs) - if isinstance(frs['roadCameraState'], NumpyFrameReader): - del frs['roadCameraState'].frames - del frs['wideRoadCameraState'].frames dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs) msgs = modeld_msgs + dmonitoringmodeld_msgs @@ -193,34 +190,36 @@ def model_replay(lr, frs): print("----------------- Model Timing -----------------") print("------------------------------------------------") print(tabulate(rows, header, tablefmt="simple_grid", stralign="center", numalign="center", floatfmt=".4f")) - assert timings_ok + assert timings_ok or PC return msgs def get_frames(): regen_cache = "--regen-cache" in sys.argv - cache = "--cache" in sys.argv or not PC or regen_cache - videos = ('fcamera.hevc', 'dcamera.hevc', 'ecamera.hevc') - cams = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState') + frames_cache = '/tmp/model_replay_cache' if PC else '/data/model_replay_cache' + os.makedirs(frames_cache, exist_ok=True) - if cache: - frames_cache = '/tmp/model_replay_cache' if PC else '/data/model_replay_cache' - os.makedirs(frames_cache, exist_ok=True) - - cache_size = 200 - for v in videos: - if not all(os.path.isfile(f'{frames_cache}/{TEST_ROUTE}_{v}_{i}.npy') for i in range(MAX_FRAMES//cache_size)) or regen_cache: - f = FrameReader(get_url(TEST_ROUTE, SEGMENT, v)).get(0, MAX_FRAMES + 1, pix_fmt="nv12") - print(f'Caching {v}...') - for i in range(MAX_FRAMES//cache_size): - np.save(f'{frames_cache}/{TEST_ROUTE}_{v}_{i}', f[(i * cache_size) + 1:((i + 1) * cache_size) + 1]) - del f - - return {c : NumpyFrameReader(f"{frames_cache}/{TEST_ROUTE}_{v}", 1928, 1208, cache_size) for c,v in zip(cams, videos, strict=True)} - else: - return {c : FrameReader(get_url(TEST_ROUTE, SEGMENT, v), readahead=True) for c,v in zip(cams, videos, strict=True)} + cache_name = f'{frames_cache}/{TEST_ROUTE}_{SEGMENT}_{START_FRAME}_{END_FRAME}.pkl' + if os.path.isfile(cache_name) and not regen_cache: + try: + print(f"Loading frames from cache {cache_name}") + return pickle.load(open(cache_name, "rb")) + except Exception as e: + print(f"Failed to load frames from cache {cache_name}: {e}") + frs = { + 'roadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "fcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME), + 'driverCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "dcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME), + 'wideRoadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "ecamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME), + } + for fr in frs.values(): + for fidx in range(START_FRAME, END_FRAME): + fr.get(fidx) + fr.it = None + print(f"Dumping frame cache {cache_name}") + pickle.dump(frs, open(cache_name, "wb")) + return frs if __name__ == "__main__": update = "--update" in sys.argv or (os.getenv("GIT_BRANCH", "") == 'master') @@ -232,8 +231,7 @@ if __name__ == "__main__": log_msgs = [] # run replays - if not NO_MODEL: - log_msgs += model_replay(lr, frs) + log_msgs += model_replay(lr, frs) # get diff failed = False @@ -242,13 +240,10 @@ if __name__ == "__main__": try: all_logs = list(LogReader(GITHUB.get_file_url(MODEL_REPLAY_BUCKET, log_fn))) cmp_log = [] - - # logs are ordered based on type: modelV2, drivingModelData, driverStateV2 - if not NO_MODEL: - model_start_index = next(i for i, m in enumerate(all_logs) if m.which() in ("modelV2", "drivingModelData", "cameraOdometry")) - cmp_log += all_logs[model_start_index:model_start_index + MAX_FRAMES*3] - dmon_start_index = next(i for i, m in enumerate(all_logs) if m.which() == "driverStateV2") - cmp_log += all_logs[dmon_start_index:dmon_start_index + MAX_FRAMES] + model_start_index = next(i for i, m in enumerate(all_logs) if m.which() in ("modelV2", "drivingModelData", "cameraOdometry")) + cmp_log += all_logs[model_start_index+START_FRAME*3:model_start_index + END_FRAME*3] + dmon_start_index = next(i for i, m in enumerate(all_logs) if m.which() == "driverStateV2") + cmp_log += all_logs[dmon_start_index+START_FRAME:dmon_start_index + END_FRAME] ignore = [ 'logMonoTime', diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index be7411ef70..bc5ccf7845 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -27,7 +27,7 @@ from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera from openpilot.selfdrive.test.process_replay.migration import migrate_all from openpilot.selfdrive.test.process_replay.capture import ProcessOutputCapture from openpilot.tools.lib.logreader import LogIterable -from openpilot.tools.lib.framereader import BaseFrameReader +from openpilot.tools.lib.framereader import FrameReader # Numpy gives different results based on CPU features after version 19 NUMPY_TOLERANCE = 1e-7 @@ -209,6 +209,7 @@ class ProcessContainer: streams_metas = available_streams(all_msgs) for meta in streams_metas: if meta.camera_state in self.cfg.vision_pubs: + assert frs[meta.camera_state].pix_fmt == 'nv12' frame_size = (frs[meta.camera_state].w, frs[meta.camera_state].h) vipc_server.create_buffers(meta.stream, 2, *frame_size) vipc_server.start_listener() @@ -224,7 +225,7 @@ class ProcessContainer: def start( self, params_config: dict[str, Any], environ_config: dict[str, Any], - all_msgs: LogIterable, frs: dict[str, BaseFrameReader] | None, + all_msgs: LogIterable, frs: dict[str, FrameReader] | None, fingerprint: str | None, capture_output: bool ): with self.prefix as p: @@ -266,7 +267,7 @@ class ProcessContainer: self.prefix.clean_dirs() self._clean_env() - def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, BaseFrameReader] | None) -> list[capnp._DynamicStructReader]: + 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 = [] @@ -296,7 +297,7 @@ class ProcessContainer: camera_state = getattr(m, m.which()) camera_meta = meta_from_camera_state(m.which()) assert frs is not None - img = frs[m.which()].get(camera_state.frameId, pix_fmt="nv12")[0] + img = frs[m.which()].get(camera_state.frameId) self.vipc_server.send(camera_meta.stream, img.flatten().tobytes(), camera_state.frameId, camera_state.timestampSof, camera_state.timestampEof) self.msg_queue = [] @@ -363,7 +364,7 @@ def get_car_params_callback(rc, pm, msgs, fingerprint): 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"), cached_params=cached_params) + _CI = get_car(*can_callbacks, 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()) @@ -655,7 +656,7 @@ def replay_process_with_name(name: str | Iterable[str], lr: LogIterable, *args, def replay_process( - cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] = None, + cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] = None, fingerprint: str = None, return_all_logs: bool = False, custom_params: dict[str, Any] = None, captured_output_store: dict[str, dict[str, str]] = None, disable_progress: bool = False ) -> list[capnp._DynamicStructReader]: @@ -683,7 +684,7 @@ def replay_process( def _replay_multi_process( - cfgs: list[ProcessConfig], lr: LogIterable, frs: dict[str, BaseFrameReader] | None, fingerprint: str | None, + cfgs: list[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] | None, fingerprint: str | None, custom_params: dict[str, Any] | None, captured_output_store: dict[str, dict[str, str]] | None, disable_progress: bool ) -> list[capnp._DynamicStructReader]: if fingerprint is not None: diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index e692f56eba..f839238098 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -9e2fe2942fbf77f24bccdbef15893831f9c0b390 \ No newline at end of file +f440c9e0469d32d350aa99ddaa8f44591a2ce690 \ No newline at end of file diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py index 4d00613f0a..ec35a5c3ac 100755 --- a/selfdrive/test/process_replay/regen.py +++ b/selfdrive/test/process_replay/regen.py @@ -3,40 +3,17 @@ import os import argparse import time import capnp -import numpy as np from typing import Any from collections.abc import Iterable from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, ProcessConfig, replay_process, get_process_config, \ check_openpilot_enabled, check_most_messages_valid, get_custom_params_from_lr -from openpilot.selfdrive.test.process_replay.vision_meta import DRIVER_CAMERA_FRAME_SIZES from openpilot.selfdrive.test.update_ci_routes import upload_route -from openpilot.tools.lib.framereader import FrameReader, BaseFrameReader, FrameType +from openpilot.tools.lib.framereader import FrameReader from openpilot.tools.lib.logreader import LogReader, LogIterable, save_log from openpilot.tools.lib.openpilotci import get_url -class DummyFrameReader(BaseFrameReader): - def __init__(self, w: int, h: int, frame_count: int, pix_val: int): - self.pix_val = pix_val - self.w, self.h = w, h - self.frame_count = frame_count - self.frame_type = FrameType.raw - - def get(self, idx, count=1, pix_fmt="yuv420p"): - if pix_fmt == "rgb24": - shape = (self.h, self.w, 3) - elif pix_fmt == "nv12" or pix_fmt == "yuv420p": - shape = (int((self.h * self.w) * 3 / 2),) - else: - raise NotImplementedError - - return [np.full(shape, self.pix_val, dtype=np.uint8) for _ in range(count)] - - @staticmethod - def zero_dcamera(): - return DummyFrameReader(*DRIVER_CAMERA_FRAME_SIZES[("tici", "ar0231")], 1200, 0) - def regen_segment( lr: LogIterable, frs: dict[str, Any] = None, @@ -64,7 +41,7 @@ def setup_data_readers( frs['wideRoadCameraState'] = FrameReader(get_url(route, str(sidx), "ecamera.hevc")) if needs_driver_cam: if dummy_driver_cam: - frs['driverCameraState'] = DummyFrameReader.zero_dcamera() + frs['driverCameraState'] = FrameReader(get_url(route, str(sidx), "fcamera.hevc")) # Use fcam as dummy else: device_type = next(str(msg.initData.deviceType) for msg in lr if msg.which() == "initData") assert device_type != "neo", "Driver camera not supported on neo segments. Use dummy dcamera." diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 14e1b2b79f..39c1e408eb 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -19,7 +19,6 @@ from openpilot.tools.lib.logreader import LogReader, save_log IS_AZURE_TOKEN_DEFINED = os.getenv("AZURE_TOKEN") source_segments = [ - ("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.COMMA_BODY ("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.HYUNDAI_SONATA ("HYUNDAI2", "d545129f3ca90f28|2022-11-07--20-43-08--3"), # HYUNDAI.HYUNDAI_KIA_EV6 (+ QCOM GPS) ("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.TOYOTA_PRIUS @@ -44,7 +43,6 @@ source_segments = [ ] segments = [ - ("BODY", "regen2F3C7259F1B|2025-04-08--23-00-23--0"), ("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"), ("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"), ("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"), @@ -65,7 +63,7 @@ segments = [ ] # dashcamOnly makes don't need to be tested until a full port is done -excluded_interfaces = ["mock", "tesla"] +excluded_interfaces = ["mock", "body"] BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/" REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit") @@ -253,7 +251,7 @@ if __name__ == "__main__": continue # to speed things up, we only test all segments on card - if cfg.proc_name != 'card' and car_brand not in ('HYUNDAI', 'TOYOTA', 'HONDA', 'SUBARU', 'FORD', 'RIVIAN', 'TESLA'): + if cfg.proc_name not in ('card', 'controlsd', 'lagd') and car_brand not in ('HYUNDAI', 'TOYOTA'): continue cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.zst") diff --git a/selfdrive/test/process_replay/test_regen.py b/selfdrive/test/process_replay/test_regen.py index 6c4b48c8d5..5f26daf786 100644 --- a/selfdrive/test/process_replay/test_regen.py +++ b/selfdrive/test/process_replay/test_regen.py @@ -1,6 +1,6 @@ from parameterized import parameterized -from openpilot.selfdrive.test.process_replay.regen import regen_segment, DummyFrameReader +from openpilot.selfdrive.test.process_replay.regen import regen_segment from openpilot.selfdrive.test.process_replay.process_replay import check_openpilot_enabled from openpilot.tools.lib.openpilotci import get_url from openpilot.tools.lib.logreader import LogReader @@ -18,7 +18,7 @@ def ci_setup_data_readers(route, sidx): lr = LogReader(get_url(route, sidx, "rlog.bz2")) frs = { 'roadCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")), - 'driverCameraState': DummyFrameReader.zero_dcamera() + 'driverCameraState': FrameReader(get_url(route, sidx, "fcamera.hevc")), } if next((True for m in lr if m.which() == "wideRoadCameraState"), False): frs["wideRoadCameraState"] = FrameReader(get_url(route, sidx, "ecamera.hevc")) diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index d0c725cacb..f98e3bd88c 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -40,14 +40,14 @@ PROCS = { "selfdrive.selfdrived.selfdrived": 16.0, "selfdrive.car.card": 26.0, "./loggerd": 14.0, - "./encoderd": 17.0, + "./encoderd": 13.0, "./camerad": 10.0, - "selfdrive.controls.plannerd": 9.0, + "selfdrive.controls.plannerd": 8.0, "./ui": 18.0, - "./sensord": 7.0, + "system.sensord.sensord": 13.0, "selfdrive.controls.radard": 2.0, "selfdrive.modeld.modeld": 22.0, - "selfdrive.modeld.dmonitoringmodeld": 21.0, + "selfdrive.modeld.dmonitoringmodeld": 18.0, "system.hardware.hardwared": 4.0, "selfdrive.locationd.calibrationd": 2.0, "selfdrive.locationd.torqued": 5.0, diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 1e8ebb957f..d883ca5b15 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -1,6 +1,5 @@ -import os import json -Import('qt_env', 'arch', 'common', 'messaging', 'visionipc', 'transformations') +Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'transformations') base_libs = [common, messaging, visionipc, transformations, 'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] @@ -83,8 +82,15 @@ if GetOption('extras'): if arch != "Darwin": # build installers - senv = qt_env.Clone() - senv['LINKFLAGS'].append('-Wl,-strip-debug') + raylib_env = env.Clone() + raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/'] + raylib_env['LINKFLAGS'].append('-Wl,-strip-debug') + + raylib_libs = common + ["raylib"] + if arch == "larch64": + raylib_libs += ["GLESv2", "wayland-client", "wayland-egl", "EGL"] + else: + raylib_libs += ["GL"] release = "release3" installers = [ @@ -94,17 +100,19 @@ if GetOption('extras'): ("openpilot_internal", "nightly-dev"), ] - cont = senv.Command(f"installer/continue_openpilot.o", f"installer/continue_openpilot.sh", - "ld -r -b binary -o $TARGET $SOURCE") + cont = raylib_env.Command("installer/continue_openpilot.o", "installer/continue_openpilot.sh", + "ld -r -b binary -o $TARGET $SOURCE") + inter = raylib_env.Command("installer/inter_ttf.o", "installer/inter-ascii.ttf", + "ld -r -b binary -o $TARGET $SOURCE") for name, branch in installers: d = {'BRANCH': f"'\"{branch}\"'"} if "internal" in name: d['INTERNAL'] = "1" - obj = senv.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d) - f = senv.Program(f"installer/installers/installer_{name}", [obj, cont], LIBS=qt_libs) + obj = raylib_env.Object(f"installer/installers/installer_{name}.o", ["installer/installer.cc"], CPPDEFINES=d) + f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs) # keep installers small - assert f[0].get_size() < 370*1e3 + assert f[0].get_size() < 1900*1e3, f[0].get_size() # build watch3 if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'): diff --git a/selfdrive/ui/installer/installer.cc b/selfdrive/ui/installer/installer.cc index 84486e61be..7326e089ab 100644 --- a/selfdrive/ui/installer/installer.cc +++ b/selfdrive/ui/installer/installer.cc @@ -1,19 +1,15 @@ -#include - -#include +#include +#include #include #include -#include - -#include -#include -#include -#include +#include "common/swaglog.h" #include "common/util.h" -#include "selfdrive/ui/installer/installer.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/qt_window.h" +#include "third_party/raylib/include/raylib.h" + +int freshClone(); +int cachedFetch(const std::string &cache); +int executeGitCommand(const std::string &cmd); std::string get_str(std::string const s) { std::string::size_type pos = s.find('?'); @@ -28,136 +24,108 @@ const std::string BRANCH_STR = get_str(BRANCH "? #define GIT_SSH_URL "git@github.com:commaai/openpilot.git" #define CONTINUE_PATH "/data/continue.sh" -const QString CACHE_PATH = "/data/openpilot.cache"; +const std::string CACHE_PATH = "/data/openpilot.cache"; #define INSTALL_PATH "/data/openpilot" #define TMP_INSTALL_PATH "/data/tmppilot" extern const uint8_t str_continue[] asm("_binary_selfdrive_ui_installer_continue_openpilot_sh_start"); extern const uint8_t str_continue_end[] asm("_binary_selfdrive_ui_installer_continue_openpilot_sh_end"); +extern const uint8_t inter_ttf[] asm("_binary_selfdrive_ui_installer_inter_ascii_ttf_start"); +extern const uint8_t inter_ttf_end[] asm("_binary_selfdrive_ui_installer_inter_ascii_ttf_end"); + +Font font; void run(const char* cmd) { int err = std::system(cmd); assert(err == 0); } -Installer::Installer(QWidget *parent) : QWidget(parent) { - QVBoxLayout *layout = new QVBoxLayout(this); - layout->setContentsMargins(150, 290, 150, 150); - layout->setSpacing(0); - - QLabel *title = new QLabel(tr("Installing...")); - title->setStyleSheet("font-size: 90px; font-weight: 600;"); - layout->addWidget(title, 0, Qt::AlignTop); - - layout->addSpacing(170); - - bar = new QProgressBar(); - bar->setRange(0, 100); - bar->setTextVisible(false); - bar->setFixedHeight(72); - layout->addWidget(bar, 0, Qt::AlignTop); - - layout->addSpacing(30); - - val = new QLabel("0%"); - val->setStyleSheet("font-size: 70px; font-weight: 300;"); - layout->addWidget(val, 0, Qt::AlignTop); - - layout->addStretch(); - - QObject::connect(&proc, QOverload::of(&QProcess::finished), this, &Installer::cloneFinished); - QObject::connect(&proc, &QProcess::readyReadStandardError, this, &Installer::readProgress); - - QTimer::singleShot(100, this, &Installer::doInstall); - - setStyleSheet(R"( - * { - font-family: Inter; - color: white; - background-color: black; - } - QProgressBar { - border: none; - background-color: #292929; - } - QProgressBar::chunk { - background-color: #364DEF; - } - )"); +void renderProgress(int progress) { + BeginDrawing(); + ClearBackground(BLACK); + DrawTextEx(font, "Installing...", (Vector2){150, 290}, 110, 0, WHITE); + Rectangle bar = {150, 570, (float)GetScreenWidth() - 300, 72}; + DrawRectangleRec(bar, (Color){41, 41, 41, 255}); + progress = std::clamp(progress, 0, 100); + bar.width *= progress / 100.0f; + DrawRectangleRec(bar, (Color){70, 91, 234, 255}); + DrawTextEx(font, (std::to_string(progress) + "%").c_str(), (Vector2){150, 670}, 85, 0, WHITE); + EndDrawing(); } -void Installer::updateProgress(int percent) { - bar->setValue(percent); - val->setText(QString("%1%").arg(percent)); - update(); -} - -void Installer::doInstall() { +int doInstall() { // wait for valid time while (!util::system_time_valid()) { - usleep(500 * 1000); - qDebug() << "Waiting for valid time"; + util::sleep_for(500); + LOGD("Waiting for valid time"); } // cleanup previous install attempts run("rm -rf " TMP_INSTALL_PATH " " INSTALL_PATH); // do the install - if (QDir(CACHE_PATH).exists()) { - cachedFetch(CACHE_PATH); + if (util::file_exists(CACHE_PATH)) { + return cachedFetch(CACHE_PATH); } else { - freshClone(); + return freshClone(); } } -void Installer::freshClone() { - qDebug() << "Doing fresh clone"; - proc.start("git", {"clone", "--progress", GIT_URL.c_str(), "-b", BRANCH_STR.c_str(), - "--depth=1", "--recurse-submodules", TMP_INSTALL_PATH}); +int freshClone() { + LOGD("Doing fresh clone"); + std::string cmd = util::string_format("git clone --progress %s -b %s --depth=1 --recurse-submodules %s 2>&1", + GIT_URL.c_str(), BRANCH_STR.c_str(), TMP_INSTALL_PATH); + return executeGitCommand(cmd); } -void Installer::cachedFetch(const QString &cache) { - qDebug() << "Fetching with cache: " << cache; +int cachedFetch(const std::string &cache) { + LOGD("Fetching with cache: %s", cache.c_str()); - run(QString("cp -rp %1 %2").arg(cache, TMP_INSTALL_PATH).toStdString().c_str()); - int err = chdir(TMP_INSTALL_PATH); - assert(err == 0); - run(("git remote set-branches --add origin " + BRANCH_STR).c_str()); + run(util::string_format("cp -rp %s %s", cache.c_str(), TMP_INSTALL_PATH).c_str()); + run(util::string_format("cd %s && git remote set-branches --add origin %s", TMP_INSTALL_PATH, BRANCH_STR.c_str()).c_str()); - updateProgress(10); + renderProgress(10); - proc.setWorkingDirectory(TMP_INSTALL_PATH); - proc.start("git", {"fetch", "--progress", "origin", BRANCH_STR.c_str()}); + return executeGitCommand(util::string_format("cd %s && git fetch --progress origin %s 2>&1", TMP_INSTALL_PATH, BRANCH_STR.c_str())); } -void Installer::readProgress() { - const QVector> stages = { +int executeGitCommand(const std::string &cmd) { + static const std::array stages = { // prefix, weight in percentage - {"Receiving objects: ", 91}, - {"Resolving deltas: ", 2}, - {"Updating files: ", 7}, + std::pair{"Receiving objects: ", 91}, + std::pair{"Resolving deltas: ", 2}, + std::pair{"Updating files: ", 7}, }; - auto line = QString(proc.readAllStandardError()); + FILE *pipe = popen(cmd.c_str(), "r"); + if (!pipe) return -1; - int base = 0; - for (const QPair kv : stages) { - if (line.startsWith(kv.first)) { - auto perc = line.split(kv.first)[1].split("%")[0]; - int p = base + int(perc.toFloat() / 100. * kv.second); - updateProgress(p); - break; + char buffer[512]; + while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { + std::string line(buffer); + int base = 0; + for (const auto &[text, weight] : stages) { + if (line.find(text) != std::string::npos) { + size_t percentPos = line.find("%"); + if (percentPos != std::string::npos && percentPos >= 3) { + int percent = std::stoi(line.substr(percentPos - 3, 3)); + int progress = base + int(percent / 100. * weight); + renderProgress(progress); + } + break; + } + base += weight; } - base += kv.second; } + return pclose(pipe); } -void Installer::cloneFinished(int exitCode, QProcess::ExitStatus exitStatus) { - qDebug() << "git finished with " << exitCode; +void cloneFinished(int exitCode) { + LOGD("git finished with %d", exitCode); assert(exitCode == 0); - updateProgress(100); + renderProgress(100); // ensure correct branch is checked out int err = chdir(TMP_INSTALL_PATH); @@ -203,13 +171,17 @@ void Installer::cloneFinished(int exitCode, QProcess::ExitStatus exitStatus) { run("mv /data/continue.sh.new " CONTINUE_PATH); // wait for the installed software's UI to take over - QTimer::singleShot(60 * 1000, &QCoreApplication::quit); + util::sleep_for(60 * 1000); } int main(int argc, char *argv[]) { - initApp(argc, argv); - QApplication a(argc, argv); - Installer installer; - setMainWindow(&installer); - return a.exec(); + InitWindow(2160, 1080, "Installer"); + font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, 120, NULL, 0); + SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR); + renderProgress(0); + int result = doInstall(); + cloneFinished(result); + CloseWindow(); + UnloadFont(font); + return 0; } diff --git a/selfdrive/ui/installer/installer.h b/selfdrive/ui/installer/installer.h deleted file mode 100644 index de3af0ff39..0000000000 --- a/selfdrive/ui/installer/installer.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -class Installer : public QWidget { - Q_OBJECT - -public: - explicit Installer(QWidget *parent = 0); - -private slots: - void updateProgress(int percent); - - void readProgress(); - void cloneFinished(int exitCode, QProcess::ExitStatus exitStatus); - -private: - QLabel *val; - QProgressBar *bar; - QProcess proc; - - void doInstall(); - void freshClone(); - void cachedFetch(const QString &cache); -}; diff --git a/selfdrive/ui/installer/inter-ascii.ttf b/selfdrive/ui/installer/inter-ascii.ttf new file mode 100644 index 0000000000..5d480c515a --- /dev/null +++ b/selfdrive/ui/installer/inter-ascii.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ef26a4099ef867f3493389379d882381a2491cdbfa41a086be8899a2154dcb3 +size 26160 diff --git a/selfdrive/ui/layouts/__init__.py b/selfdrive/ui/layouts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/selfdrive/ui/layouts/home.py b/selfdrive/ui/layouts/home.py new file mode 100644 index 0000000000..0fa91da552 --- /dev/null +++ b/selfdrive/ui/layouts/home.py @@ -0,0 +1,214 @@ +import time +import pyray as rl +from collections.abc import Callable +from enum import IntEnum +from openpilot.common.params import Params +from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert +from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButton +from openpilot.selfdrive.ui.widgets.prime import PrimeWidget +from openpilot.selfdrive.ui.widgets.setup import SetupWidget +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR +from openpilot.system.ui.lib.widget import Widget + +HEADER_HEIGHT = 80 +HEAD_BUTTON_FONT_SIZE = 40 +CONTENT_MARGIN = 40 +SPACING = 25 +RIGHT_COLUMN_WIDTH = 750 +REFRESH_INTERVAL = 10.0 + +PRIME_BG_COLOR = rl.Color(51, 51, 51, 255) + + +class HomeLayoutState(IntEnum): + HOME = 0 + UPDATE = 1 + ALERTS = 2 + + +class HomeLayout(Widget): + def __init__(self): + super().__init__() + self.params = Params() + + self.update_alert = UpdateAlert() + self.offroad_alert = OffroadAlert() + + self.current_state = HomeLayoutState.HOME + self.last_refresh = 0 + self.settings_callback: callable | None = None + + self.update_available = False + self.alert_count = 0 + + self.header_rect = rl.Rectangle(0, 0, 0, 0) + self.content_rect = rl.Rectangle(0, 0, 0, 0) + self.left_column_rect = rl.Rectangle(0, 0, 0, 0) + self.right_column_rect = rl.Rectangle(0, 0, 0, 0) + + self.update_notif_rect = rl.Rectangle(0, 0, 200, HEADER_HEIGHT - 10) + self.alert_notif_rect = rl.Rectangle(0, 0, 220, HEADER_HEIGHT - 10) + + self._prime_widget = PrimeWidget() + self._setup_widget = SetupWidget() + + self._exp_mode_button = ExperimentalModeButton() + self._setup_callbacks() + + def _setup_callbacks(self): + self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME)) + self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME)) + + def set_settings_callback(self, callback: Callable): + self.settings_callback = callback + + def _set_state(self, state: HomeLayoutState): + self.current_state = state + + def _render(self, rect: rl.Rectangle): + current_time = time.time() + if current_time - self.last_refresh >= REFRESH_INTERVAL: + self._refresh() + self.last_refresh = current_time + + self._handle_input() + self._render_header() + + # Render content based on current state + if self.current_state == HomeLayoutState.HOME: + self._render_home_content() + elif self.current_state == HomeLayoutState.UPDATE: + self._render_update_view() + elif self.current_state == HomeLayoutState.ALERTS: + self._render_alerts_view() + + def _update_layout_rects(self): + self.header_rect = rl.Rectangle( + self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT + ) + + content_y = self._rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING + content_height = self._rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN + + self.content_rect = rl.Rectangle( + self._rect.x + CONTENT_MARGIN, content_y, self._rect.width - 2 * CONTENT_MARGIN, content_height + ) + + left_width = self.content_rect.width - RIGHT_COLUMN_WIDTH - SPACING + + self.left_column_rect = rl.Rectangle(self.content_rect.x, self.content_rect.y, left_width, self.content_rect.height) + + self.right_column_rect = rl.Rectangle( + self.content_rect.x + left_width + SPACING, self.content_rect.y, RIGHT_COLUMN_WIDTH, self.content_rect.height + ) + + self.update_notif_rect.x = self.header_rect.x + self.update_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2 + + notif_x = self.header_rect.x + (220 if self.update_available else 0) + self.alert_notif_rect.x = notif_x + self.alert_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2 + + def _handle_input(self): + if not rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): + return + + mouse_pos = rl.get_mouse_position() + + if self.update_available and rl.check_collision_point_rec(mouse_pos, self.update_notif_rect): + self._set_state(HomeLayoutState.UPDATE) + return + + if self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect): + self._set_state(HomeLayoutState.ALERTS) + return + + # Content area input handling + if self.current_state == HomeLayoutState.UPDATE: + self.update_alert.handle_input(mouse_pos, True) + elif self.current_state == HomeLayoutState.ALERTS: + self.offroad_alert.handle_input(mouse_pos, True) + + def _render_header(self): + font = gui_app.font(FontWeight.MEDIUM) + + # Update notification button + if self.update_available: + # Highlight if currently viewing updates + highlight_color = rl.Color(255, 140, 40, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(255, 102, 0, 255) + rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color) + + text = "UPDATE" + text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x + text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2 + text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2 + rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE) + + # Alert notification button + if self.alert_count > 0: + # Highlight if currently viewing alerts + highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255) + rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color) + + alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}" + text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x + text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2 + text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2 + rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE) + + # Version text (right aligned) + version_text = self._get_version_text() + text_width = measure_text_cached(gui_app.font(FontWeight.NORMAL), version_text, 48).x + version_x = self.header_rect.x + self.header_rect.width - text_width + version_y = self.header_rect.y + (self.header_rect.height - 48) // 2 + rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), version_text, rl.Vector2(int(version_x), int(version_y)), 48, 0, DEFAULT_TEXT_COLOR) + + def _render_home_content(self): + self._render_left_column() + self._render_right_column() + + def _render_update_view(self): + self.update_alert.render(self.content_rect) + + def _render_alerts_view(self): + self.offroad_alert.render(self.content_rect) + + def _render_left_column(self): + self._prime_widget.render(self.left_column_rect) + + def _render_right_column(self): + exp_height = 125 + exp_rect = rl.Rectangle( + self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, exp_height + ) + self._exp_mode_button.render(exp_rect) + + setup_rect = rl.Rectangle( + self.right_column_rect.x, + self.right_column_rect.y + exp_height + SPACING, + self.right_column_rect.width, + self.right_column_rect.height - exp_height - SPACING, + ) + self._setup_widget.render(setup_rect) + + def _refresh(self): + # TODO: implement _update_state with a timer + self.update_available = self.update_alert.refresh() + self.alert_count = self.offroad_alert.refresh() + self._update_state_priority(self.update_available, self.alert_count > 0) + + def _update_state_priority(self, update_available: bool, alerts_present: bool): + current_state = self.current_state + + if not update_available and not alerts_present: + self.current_state = HomeLayoutState.HOME + elif update_available and (current_state == HomeLayoutState.HOME or (not alerts_present and current_state == HomeLayoutState.ALERTS)): + self.current_state = HomeLayoutState.UPDATE + elif alerts_present and (current_state == HomeLayoutState.HOME or (not update_available and current_state == HomeLayoutState.UPDATE)): + self.current_state = HomeLayoutState.ALERTS + + def _get_version_text(self) -> str: + brand = "openpilot" + description = self.params.get("UpdaterCurrentDescription", encoding='utf-8') + return f"{brand} {description}" if description else brand diff --git a/selfdrive/ui/layouts/main.py b/selfdrive/ui/layouts/main.py new file mode 100644 index 0000000000..a3081143f4 --- /dev/null +++ b/selfdrive/ui/layouts/main.py @@ -0,0 +1,93 @@ +import pyray as rl +from enum import IntEnum +import cereal.messaging as messaging +from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH +from openpilot.selfdrive.ui.layouts.home import HomeLayout +from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType +from openpilot.selfdrive.ui.ui_state import device, ui_state +from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView +from openpilot.system.ui.lib.widget import Widget + + +class MainState(IntEnum): + HOME = 0 + SETTINGS = 1 + ONROAD = 2 + + +class MainLayout(Widget): + def __init__(self): + super().__init__() + + self._pm = messaging.PubMaster(['userFlag']) + + self._sidebar = Sidebar() + self._current_mode = MainState.HOME + self._prev_onroad = False + + # Initialize layouts + self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()} + + self._sidebar_rect = rl.Rectangle(0, 0, 0, 0) + self._content_rect = rl.Rectangle(0, 0, 0, 0) + + # Set callbacks + self._setup_callbacks() + + def _render(self, _): + self._handle_onroad_transition() + self._render_main_content() + + def _setup_callbacks(self): + self._sidebar.set_callbacks(on_settings=self._on_settings_clicked, + on_flag=self._on_flag_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) + device.add_interactive_timeout_callback(self._set_mode_for_state) + + def _update_layout_rects(self): + self._sidebar_rect = rl.Rectangle(self._rect.x, self._rect.y, SIDEBAR_WIDTH, self._rect.height) + + x_offset = SIDEBAR_WIDTH if self._sidebar.is_visible else 0 + self._content_rect = rl.Rectangle(self._rect.y + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height) + + def _handle_onroad_transition(self): + if ui_state.started != self._prev_onroad: + self._prev_onroad = ui_state.started + + self._set_mode_for_state() + + def _set_mode_for_state(self): + if ui_state.started: + # Don't hide sidebar from interactive timeout + if self._current_mode != MainState.ONROAD: + self._sidebar.set_visible(False) + self._current_mode = MainState.ONROAD + else: + self._current_mode = MainState.HOME + self._sidebar.set_visible(True) + + def open_settings(self, panel_type: PanelType): + self._layouts[MainState.SETTINGS].set_current_panel(panel_type) + self._current_mode = MainState.SETTINGS + self._sidebar.set_visible(False) + + 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_onroad_clicked(self): + self._sidebar.set_visible(not self._sidebar.is_visible) + + def _render_main_content(self): + # Render sidebar + if self._sidebar.is_visible: + self._sidebar.render(self._sidebar_rect) + + content_rect = self._content_rect if self._sidebar.is_visible else self._rect + self._layouts[self._current_mode].render(content_rect) diff --git a/selfdrive/ui/layouts/network.py b/selfdrive/ui/layouts/network.py new file mode 100644 index 0000000000..c227facd04 --- /dev/null +++ b/selfdrive/ui/layouts/network.py @@ -0,0 +1,17 @@ +import pyray as rl +from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper +from openpilot.system.ui.widgets.network import WifiManagerUI + + +class NetworkLayout(Widget): + def __init__(self): + super().__init__() + self.wifi_manager = WifiManagerWrapper() + self.wifi_ui = WifiManagerUI(self.wifi_manager) + + def _render(self, rect: rl.Rectangle): + self.wifi_ui.render(rect) + + def shutdown(self): + self.wifi_manager.shutdown() diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py new file mode 100644 index 0000000000..ce6e9e4af6 --- /dev/null +++ b/selfdrive/ui/layouts/settings/developer.py @@ -0,0 +1,61 @@ +from openpilot.system.ui.lib.list_view import toggle_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget +from openpilot.common.params import Params +from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item + +# Description constants +DESCRIPTIONS = { + 'enable_adb': ( + "ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. " + + "See https://docs.comma.ai/how-to/connect-to-comma for more info." + ), + 'joystick_debug_mode': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)", + 'ssh_key': ( + "Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " + + "other than your own. A comma employee will NEVER ask you to add their GitHub username." + ), +} + + +class DeveloperLayout(Widget): + def __init__(self): + super().__init__() + self._params = Params() + items = [ + toggle_item( + "Enable ADB", + description=DESCRIPTIONS["enable_adb"], + initial_state=self._params.get_bool("AdbEnabled"), + callback=self._on_enable_adb, + ), + ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]), + toggle_item( + "Joystick Debug Mode", + description=DESCRIPTIONS["joystick_debug_mode"], + initial_state=self._params.get_bool("JoystickDebugMode"), + callback=self._on_joystick_debug_mode, + ), + toggle_item( + "Longitudinal Maneuver Mode", + description="", + initial_state=self._params.get_bool("LongitudinalManeuverMode"), + callback=self._on_long_maneuver_mode, + ), + toggle_item( + "openpilot Longitudinal Control (Alpha)", + description="", + initial_state=self._params.get_bool("AlphaLongitudinalEnabled"), + callback=self._on_alpha_long_enabled, + ), + ] + + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _render(self, rect): + self._scroller.render(rect) + + def _on_enable_adb(self): pass + def _on_joystick_debug_mode(self): pass + def _on_long_maneuver_mode(self): pass + def _on_alpha_long_enabled(self): pass diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py new file mode 100644 index 0000000000..69b7822ba9 --- /dev/null +++ b/selfdrive/ui/layouts/settings/device.py @@ -0,0 +1,150 @@ +import os +import json + +from openpilot.common.basedir import BASEDIR +from openpilot.common.params import Params +from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.hardware import TICI +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.list_view import text_item, button_item, dual_button_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget, DialogResult +from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog +from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog +from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog +from openpilot.system.ui.widgets.html_render import HtmlRenderer + +# Description constants +DESCRIPTIONS = { + 'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.", + 'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)", + 'reset_calibration': ( + "openpilot requires the device to be mounted within 4° left or right and within 5° " + + "up or 9° down. openpilot is continuously calibrating, resetting is rarely required." + ), + 'review_guide': "Review the rules, features, and limitations of openpilot", +} + + +class DeviceLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + self._select_language_dialog: MultiOptionDialog | None = None + self._driver_camera: DriverCameraDialog | None = None + self._pair_device_dialog: PairingDialog | None = None + self._fcc_dialog: HtmlRenderer | None = None + + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _initialize_items(self): + dongle_id = self._params.get("DongleId", encoding="utf-8") or "N/A" + serial = self._params.get("HardwareSerial") or "N/A" + + items = [ + text_item("Dongle ID", dongle_id), + text_item("Serial", serial), + button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device), + button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad), + button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt), + regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory), + button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide), + button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad), + dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt), + ] + regulatory_btn.set_visible(TICI) + return items + + def _render(self, rect): + self._scroller.render(rect) + + def _show_language_selection(self): + try: + languages_file = os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json") + with open(languages_file, encoding='utf-8') as f: + languages = json.load(f) + + self._select_language_dialog = MultiOptionDialog("Select a language", languages) + gui_app.set_modal_overlay(self._select_language_dialog, callback=self._handle_language_selection) + except FileNotFoundError: + pass + + def _handle_language_selection(self, result: int): + if result == 1 and self._select_language_dialog: + selected_language = self._select_language_dialog.selection + self._params.put("LanguageSetting", selected_language) + + self._select_language_dialog = None + + def _show_driver_camera(self): + if not self._driver_camera: + self._driver_camera = DriverCameraDialog() + + gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None)) + + def _reset_calibration_prompt(self): + if ui_state.engaged: + gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reset Calibration")) + return + + gui_app.set_modal_overlay( + lambda: confirm_dialog("Are you sure you want to reset calibration?", "Reset"), + callback=self._reset_calibration, + ) + + def _reset_calibration(self, result: int): + if ui_state.engaged or result != DialogResult.CONFIRM: + return + + self._params.remove("CalibrationParams") + self._params.remove("LiveTorqueParameters") + self._params.remove("LiveParameters") + self._params.remove("LiveParametersV2") + self._params.remove("LiveDelay") + self._params.put_bool("OnroadCycleRequested", True) + + def _reboot_prompt(self): + if ui_state.engaged: + gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Reboot")) + return + + gui_app.set_modal_overlay( + lambda: confirm_dialog("Are you sure you want to reboot?", "Reboot"), + callback=self._perform_reboot, + ) + + def _perform_reboot(self, result: int): + if not ui_state.engaged and result == DialogResult.CONFIRM: + self._params.put_bool_nonblocking("DoReboot", True) + + def _power_off_prompt(self): + if ui_state.engaged: + gui_app.set_modal_overlay(lambda: alert_dialog("Disengage to Power Off")) + return + + gui_app.set_modal_overlay( + lambda: confirm_dialog("Are you sure you want to power off?", "Power Off"), + callback=self._perform_power_off, + ) + + def _perform_power_off(self, result: int): + if not ui_state.engaged and result == DialogResult.CONFIRM: + self._params.put_bool_nonblocking("DoShutdown", True) + + def _pair_device(self): + if not self._pair_device_dialog: + self._pair_device_dialog = PairingDialog() + gui_app.set_modal_overlay(self._pair_device_dialog, callback=lambda result: setattr(self, '_pair_device_dialog', None)) + + def _on_regulatory(self): + if not self._fcc_dialog: + self._fcc_dialog = HtmlRenderer(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html")) + + gui_app.set_modal_overlay(self._fcc_dialog, + callback=lambda result: setattr(self, '_fcc_dialog', None), + ) + + def _on_review_training_guide(self): pass diff --git a/selfdrive/ui/layouts/settings/firehose.py b/selfdrive/ui/layouts/settings/firehose.py new file mode 100644 index 0000000000..298f66e276 --- /dev/null +++ b/selfdrive/ui/layouts/settings/firehose.py @@ -0,0 +1,180 @@ +import pyray as rl +import json +import time +import threading + +from openpilot.common.api import Api, api_get +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog +from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.lib.widget import Widget +from openpilot.selfdrive.ui.ui_state import ui_state + + +TITLE = "Firehose Mode" +DESCRIPTION = ( + "openpilot learns to drive by watching humans, like you, drive.\n\n" + + "Firehose Mode allows you to maximize your training data uploads to improve " + + "openpilot's driving models. More data means bigger models, which means better Experimental Mode." +) +INSTRUCTIONS = ( + "For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n" + + "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n" + + "Frequently Asked Questions\n\n" + + "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n" + + "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n" + + "What's a good USB-C adapter? Any fast phone or laptop charger should be fine.\n\n" + + "Does it matter which software I run? Yes, only upstream openpilot (and particular forks) are able to be used for training." +) + + +class FirehoseLayout(Widget): + PARAM_KEY = "ApiCache_FirehoseStats" + GREEN = rl.Color(46, 204, 113, 255) + RED = rl.Color(231, 76, 60, 255) + GRAY = rl.Color(68, 68, 68, 255) + LIGHT_GRAY = rl.Color(228, 228, 228, 255) + UPDATE_INTERVAL = 30 # seconds + + def __init__(self): + super().__init__() + self.params = Params() + self.segment_count = self._get_segment_count() + self.scroll_panel = GuiScrollPanel() + + self.running = True + self.update_thread = threading.Thread(target=self._update_loop, daemon=True) + self.update_thread.start() + self.last_update_time = 0 + + def _get_segment_count(self) -> int: + stats = self.params.get(self.PARAM_KEY, encoding='utf8') + if not stats: + return 0 + try: + return int(json.loads(stats).get("firehose", 0)) + except Exception: + cloudlog.exception(f"Failed to decode firehose stats: {stats}") + return 0 + + def __del__(self): + self.running = False + if self.update_thread and self.update_thread.is_alive(): + self.update_thread.join(timeout=1.0) + + def _render(self, rect: rl.Rectangle): + # Calculate content dimensions + content_width = rect.width - 80 + content_height = self._calculate_content_height(int(content_width)) + content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height) + + # Handle scrolling and render with clipping + scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect) + rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) + self._render_content(rect, scroll_offset) + rl.end_scissor_mode() + + def _calculate_content_height(self, content_width: int) -> int: + height = 80 # Top margin + + # Title + height += 100 + 40 + + # Description + desc_font = gui_app.font(FontWeight.NORMAL) + desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width) + height += len(desc_lines) * 45 + 40 + + # Status section + height += 32 # Separator + status_text, _ = self._get_status() + status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width) + height += len(status_lines) * 60 + 20 + + # Contribution count (if available) + if self.segment_count > 0: + contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far." + contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width) + height += len(contrib_lines) * 52 + 20 + + # Instructions section + height += 32 # Separator + inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width) + height += len(inst_lines) * 40 + 40 # Bottom margin + + return height + + def _render_content(self, rect: rl.Rectangle, scroll_offset: rl.Vector2): + x = int(rect.x + 40) + y = int(rect.y + 40 + scroll_offset.y) + w = int(rect.width - 80) + + # Title + title_font = gui_app.font(FontWeight.MEDIUM) + rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE) + y += 140 + + # Description + y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE) + y += 40 + + # Separator + rl.draw_rectangle(x, y, w, 2, self.GRAY) + y += 30 + + # Status + status_text, status_color = self._get_status() + y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color) + y += 20 + + # Contribution count (if available) + if self.segment_count > 0: + contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far." + y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE) + y += 20 + + # Separator + rl.draw_rectangle(x, y, w, 2, self.GRAY) + y += 30 + + # Instructions + self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY) + + def _draw_wrapped_text(self, x, y, width, text, font, size, color): + wrapped = wrap_text(font, text, size, width) + for line in wrapped: + rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color) + y += size + return y + + def _get_status(self) -> tuple[str, rl.Color]: + network_type = ui_state.sm["deviceState"].networkType + network_metered = ui_state.sm["deviceState"].networkMetered + + if not network_metered and network_type != 0: # Not metered and connected + return "ACTIVE", self.GREEN + else: + return "INACTIVE: connect to an unmetered network", self.RED + + def _fetch_firehose_stats(self): + try: + dongle_id = self.params.get("DongleId", encoding='utf8') + if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: + return + identity_token = Api(dongle_id).get_token() + response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) + if response.status_code == 200: + data = response.json() + self.segment_count = data.get("firehose", 0) + self.params.put(self.PARAM_KEY, json.dumps(data)) + except Exception as e: + cloudlog.error(f"Failed to fetch firehose stats: {e}") + + def _update_loop(self): + while self.running: + if not ui_state.started: + self._fetch_firehose_stats() + time.sleep(self.UPDATE_INTERVAL) diff --git a/selfdrive/ui/layouts/settings/settings.py b/selfdrive/ui/layouts/settings/settings.py new file mode 100644 index 0000000000..4a29f408ee --- /dev/null +++ b/selfdrive/ui/layouts/settings/settings.py @@ -0,0 +1,156 @@ +import pyray as rl +from dataclasses import dataclass +from enum import IntEnum +from collections.abc import Callable +from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout +from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout +from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout +from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout +from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.selfdrive.ui.layouts.network import NetworkLayout +from openpilot.system.ui.lib.widget import Widget + +# Import individual panels + +SETTINGS_CLOSE_TEXT = "X" +# Constants +SIDEBAR_WIDTH = 500 +CLOSE_BTN_SIZE = 200 +NAV_BTN_HEIGHT = 110 +PANEL_MARGIN = 50 + +# Colors +SIDEBAR_COLOR = rl.BLACK +PANEL_COLOR = rl.Color(41, 41, 41, 255) +CLOSE_BTN_COLOR = rl.Color(41, 41, 41, 255) +CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255) +TEXT_NORMAL = rl.Color(128, 128, 128, 255) +TEXT_SELECTED = rl.Color(255, 255, 255, 255) + + +class PanelType(IntEnum): + DEVICE = 0 + NETWORK = 1 + TOGGLES = 2 + SOFTWARE = 3 + FIREHOSE = 4 + DEVELOPER = 5 + + +@dataclass +class PanelInfo: + name: str + instance: object + button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) + + +class SettingsLayout(Widget): + def __init__(self): + super().__init__() + self._current_panel = PanelType.DEVICE + + # Panel configuration + self._panels = { + PanelType.DEVICE: PanelInfo("Device", DeviceLayout()), + PanelType.NETWORK: PanelInfo("Network", NetworkLayout()), + PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()), + PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()), + PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()), + PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()), + } + + self._font_medium = gui_app.font(FontWeight.MEDIUM) + self._font_bold = gui_app.font(FontWeight.SEMI_BOLD) + + # Callbacks + self._close_callback: Callable | None = None + + def set_callbacks(self, on_close: Callable): + self._close_callback = on_close + + def _render(self, rect: rl.Rectangle): + # Calculate layout + sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height) + panel_rect = rl.Rectangle(rect.x + SIDEBAR_WIDTH, rect.y, rect.width - SIDEBAR_WIDTH, rect.height) + + # Draw components + self._draw_sidebar(sidebar_rect) + self._draw_current_panel(panel_rect) + + def _draw_sidebar(self, rect: rl.Rectangle): + rl.draw_rectangle_rec(rect, SIDEBAR_COLOR) + + # Close button + close_btn_rect = rl.Rectangle( + rect.x + (rect.width - CLOSE_BTN_SIZE) / 2, rect.y + 45, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE + ) + + pressed = (rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and + rl.check_collision_point_rec(rl.get_mouse_position(), close_btn_rect)) + close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR + rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color) + + close_text_size = measure_text_cached(self._font_bold, SETTINGS_CLOSE_TEXT, 140) + close_text_pos = rl.Vector2( + close_btn_rect.x + (close_btn_rect.width - close_text_size.x) / 2, + close_btn_rect.y + (close_btn_rect.height - close_text_size.y) / 2, + ) + rl.draw_text_ex(self._font_bold, SETTINGS_CLOSE_TEXT, close_text_pos, 140, 0, TEXT_SELECTED) + + # Store close button rect for click detection + self._close_btn_rect = close_btn_rect + + # Navigation buttons + y = rect.y + 300 + for panel_type, panel_info in self._panels.items(): + button_rect = rl.Rectangle(rect.x + 50, y, rect.width - 150, NAV_BTN_HEIGHT) + + # Button styling + is_selected = panel_type == self._current_panel + text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL + # Draw button text (right-aligned) + text_size = measure_text_cached(self._font_medium, panel_info.name, 65) + text_pos = rl.Vector2( + button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2 + ) + rl.draw_text_ex(self._font_medium, panel_info.name, text_pos, 65, 0, text_color) + + # Store button rect for click detection + panel_info.button_rect = button_rect + + y += NAV_BTN_HEIGHT + + def _draw_current_panel(self, rect: rl.Rectangle): + rl.draw_rectangle_rounded( + rl.Rectangle(rect.x + 10, rect.y + 10, rect.width - 20, rect.height - 20), 0.04, 30, PANEL_COLOR + ) + content_rect = rl.Rectangle(rect.x + PANEL_MARGIN, rect.y + 25, rect.width - (PANEL_MARGIN * 2), rect.height - 50) + # rl.draw_rectangle_rounded(content_rect, 0.03, 30, PANEL_COLOR) + panel = self._panels[self._current_panel] + if panel.instance: + panel.instance.render(content_rect) + + def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool: + # Check close button + if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect): + if self._close_callback: + self._close_callback() + return True + + # Check navigation buttons + for panel_type, panel_info in self._panels.items(): + if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect): + self.set_current_panel(panel_type) + return True + + return False + + def set_current_panel(self, panel_type: PanelType): + if panel_type != self._current_panel: + self._current_panel = panel_type + + def close_settings(self): + if self._close_callback: + self._close_callback() diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py new file mode 100644 index 0000000000..b97e306ff9 --- /dev/null +++ b/selfdrive/ui/layouts/settings/software.py @@ -0,0 +1,42 @@ +from openpilot.common.params import Params +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.list_view import button_item, text_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget, DialogResult +from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog + + +class SoftwareLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._init_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _init_items(self): + items = [ + text_item("Current Version", ""), + button_item("Download", "CHECK", callback=self._on_download_update), + button_item("Install Update", "INSTALL", callback=self._on_install_update), + button_item("Target Branch", "SELECT", callback=self._on_select_branch), + button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall), + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def _on_download_update(self): pass + def _on_install_update(self): pass + def _on_select_branch(self): pass + + def _on_uninstall(self): + def handle_uninstall_confirmation(result): + if result == DialogResult.CONFIRM: + self._params.put_bool("DoUninstall", True) + + gui_app.set_modal_overlay( + lambda: confirm_dialog("Are you sure you want to uninstall?", "Uninstall"), + callback=handle_uninstall_confirmation, + ) diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py new file mode 100644 index 0000000000..5c17082769 --- /dev/null +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -0,0 +1,88 @@ +from openpilot.system.ui.lib.list_view import multiple_button_item, toggle_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget +from openpilot.common.params import Params + +# Description constants +DESCRIPTIONS = { + "OpenpilotEnabledToggle": ( + "Use the openpilot system for adaptive cruise control and lane keep driver assistance. " + + "Your attention is required at all times to use this feature." + ), + "DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.", + "LongitudinalPersonality": ( + "Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " + + "In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " + + "your steering wheel distance button." + ), + "IsLdwEnabled": ( + "Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " + + "without a turn signal activated while driving over 31 mph (50 km/h)." + ), + "AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.", + 'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.", + "IsMetric": "Display speed in km/h instead of mph.", +} + + +class TogglesLayout(Widget): + def __init__(self): + super().__init__() + self._params = Params() + items = [ + toggle_item( + "Enable openpilot", + DESCRIPTIONS["OpenpilotEnabledToggle"], + self._params.get_bool("OpenpilotEnabledToggle"), + icon="chffr_wheel.png", + ), + toggle_item( + "Experimental Mode", + initial_state=self._params.get_bool("ExperimentalMode"), + icon="experimental_white.png", + ), + toggle_item( + "Disengage on Accelerator Pedal", + DESCRIPTIONS["DisengageOnAccelerator"], + self._params.get_bool("DisengageOnAccelerator"), + icon="disengage_on_accelerator.png", + ), + multiple_button_item( + "Driving Personality", + DESCRIPTIONS["LongitudinalPersonality"], + buttons=["Aggressive", "Standard", "Relaxed"], + button_width=255, + callback=self._set_longitudinal_personality, + selected_index=int(self._params.get("LongitudinalPersonality") or 0), + icon="speed_limit.png" + ), + toggle_item( + "Enable Lane Departure Warnings", + DESCRIPTIONS["IsLdwEnabled"], + self._params.get_bool("IsLdwEnabled"), + icon="warning.png", + ), + toggle_item( + "Always-On Driver Monitoring", + DESCRIPTIONS["AlwaysOnDM"], + self._params.get_bool("AlwaysOnDM"), + icon="monitoring.png", + ), + toggle_item( + "Record and Upload Driver Camera", + DESCRIPTIONS["RecordFront"], + self._params.get_bool("RecordFront"), + icon="monitoring.png", + ), + toggle_item( + "Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="monitoring.png" + ), + ] + + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _render(self, rect): + self._scroller.render(rect) + + def _set_longitudinal_personality(self, button_index: int): + self._params.put("LongitudinalPersonality", str(button_index)) diff --git a/selfdrive/ui/layouts/sidebar.py b/selfdrive/ui/layouts/sidebar.py new file mode 100644 index 0000000000..13f69b79d4 --- /dev/null +++ b/selfdrive/ui/layouts/sidebar.py @@ -0,0 +1,204 @@ +import pyray as rl +import time +from dataclasses import dataclass +from collections.abc import Callable +from cereal import log +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.widget import Widget + +SIDEBAR_WIDTH = 300 +METRIC_HEIGHT = 126 +METRIC_WIDTH = 240 +METRIC_MARGIN = 30 + +SETTINGS_BTN = rl.Rectangle(50, 35, 200, 117) +HOME_BTN = rl.Rectangle(60, 860, 180, 180) + +ThermalStatus = log.DeviceState.ThermalStatus +NetworkType = log.DeviceState.NetworkType + + +# Color scheme +class Colors: + SIDEBAR_BG = rl.Color(57, 57, 57, 255) + WHITE = rl.Color(255, 255, 255, 255) + WHITE_DIM = rl.Color(255, 255, 255, 85) + GRAY = rl.Color(84, 84, 84, 255) + + # Status colors + GOOD = rl.Color(255, 255, 255, 255) + WARNING = rl.Color(218, 202, 37, 255) + DANGER = rl.Color(201, 34, 49, 255) + + # UI elements + METRIC_BORDER = rl.Color(255, 255, 255, 85) + BUTTON_NORMAL = rl.Color(255, 255, 255, 255) + BUTTON_PRESSED = rl.Color(255, 255, 255, 166) + + +NETWORK_TYPES = { + NetworkType.none: "Offline", + NetworkType.wifi: "WiFi", + NetworkType.cell2G: "2G", + NetworkType.cell3G: "3G", + NetworkType.cell4G: "LTE", + NetworkType.cell5G: "5G", + NetworkType.ethernet: "Ethernet", +} + + +@dataclass(slots=True) +class MetricData: + label: str + value: str + color: rl.Color + + def update(self, label: str, value: str, color: rl.Color): + self.label = label + self.value = value + self.color = color + + +class Sidebar(Widget): + def __init__(self): + super().__init__() + self._net_type = NETWORK_TYPES.get(NetworkType.none) + self._net_strength = 0 + + self._temp_status = MetricData("TEMP", "GOOD", Colors.GOOD) + self._panda_status = MetricData("VEHICLE", "ONLINE", Colors.GOOD) + self._connect_status = MetricData("CONNECT", "OFFLINE", Colors.WARNING) + + self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height) + self._flag_img = gui_app.texture("images/button_flag.png", HOME_BTN.width, HOME_BTN.height) + self._settings_img = gui_app.texture("images/button_settings.png", SETTINGS_BTN.width, SETTINGS_BTN.height) + self._font_regular = gui_app.font(FontWeight.NORMAL) + self._font_bold = gui_app.font(FontWeight.SEMI_BOLD) + + # Callbacks + self._on_settings_click: Callable | None = None + self._on_flag_click: Callable | None = None + + def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None): + self._on_settings_click = on_settings + self._on_flag_click = on_flag + + def _render(self, rect: rl.Rectangle): + # Background + rl.draw_rectangle_rec(rect, Colors.SIDEBAR_BG) + + self._draw_buttons(rect) + self._draw_network_indicator(rect) + self._draw_metrics(rect) + + def _update_state(self): + sm = ui_state.sm + if not sm.updated['deviceState']: + return + + device_state = sm['deviceState'] + + self._update_network_status(device_state) + self._update_temperature_status(device_state) + self._update_connection_status(device_state) + self._update_panda_status() + + def _update_network_status(self, device_state): + self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, "Unknown") + strength = device_state.networkStrength + self._net_strength = max(0, min(5, strength.raw + 1)) if strength > 0 else 0 + + def _update_temperature_status(self, device_state): + thermal_status = device_state.thermalStatus + + if thermal_status == ThermalStatus.green: + self._temp_status.update("TEMP", "GOOD", Colors.GOOD) + elif thermal_status == ThermalStatus.yellow: + self._temp_status.update("TEMP", "OK", Colors.WARNING) + else: + self._temp_status.update("TEMP", "HIGH", Colors.DANGER) + + def _update_connection_status(self, device_state): + last_ping = device_state.lastAthenaPingTime + if last_ping == 0: + self._connect_status.update("CONNECT", "OFFLINE", Colors.WARNING) + elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds + self._connect_status.update("CONNECT", "ONLINE", Colors.GOOD) + else: + self._connect_status.update("CONNECT", "ERROR", Colors.DANGER) + + def _update_panda_status(self): + if ui_state.panda_type == log.PandaState.PandaType.unknown: + self._panda_status.update("NO", "PANDA", Colors.DANGER) + else: + self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD) + + def _handle_mouse_release(self, mouse_pos: rl.Vector2): + if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN): + if self._on_settings_click: + self._on_settings_click() + elif rl.check_collision_point_rec(mouse_pos, HOME_BTN) and ui_state.started: + if self._on_flag_click: + self._on_flag_click() + + 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) + + # Settings button + settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN) + tint = Colors.BUTTON_PRESSED if settings_down else Colors.BUTTON_NORMAL + rl.draw_texture(self._settings_img, int(SETTINGS_BTN.x), int(SETTINGS_BTN.y), tint) + + # Home/Flag button + flag_pressed = mouse_down and rl.check_collision_point_rec(mouse_pos, HOME_BTN) + button_img = self._flag_img if ui_state.started else self._home_img + + tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL + rl.draw_texture(button_img, int(HOME_BTN.x), int(HOME_BTN.y), tint) + + def _draw_network_indicator(self, rect: rl.Rectangle): + # Signal strength dots + x_start = rect.x + 58 + y_pos = rect.y + 196 + dot_size = 27 + dot_spacing = 37 + + for i in range(5): + color = Colors.WHITE if i < self._net_strength else Colors.GRAY + x = int(x_start + i * dot_spacing + dot_size // 2) + y = int(y_pos + dot_size // 2) + rl.draw_circle(x, y, dot_size // 2, color) + + # Network type text + text_y = rect.y + 247 + text_pos = rl.Vector2(rect.x + 58, text_y) + rl.draw_text_ex(self._font_regular, self._net_type, text_pos, 35, 0, Colors.WHITE) + + def _draw_metrics(self, rect: rl.Rectangle): + metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)] + + for metric, y_offset in metrics: + self._draw_metric(rect, metric, rect.y + y_offset) + + def _draw_metric(self, rect: rl.Rectangle, metric: MetricData, y: float): + metric_rect = rl.Rectangle(rect.x + METRIC_MARGIN, y, METRIC_WIDTH, METRIC_HEIGHT) + # Draw colored left edge (clipped rounded rectangle) + edge_rect = rl.Rectangle(metric_rect.x + 4, metric_rect.y + 4, 100, 118) + rl.begin_scissor_mode(int(metric_rect.x + 4), int(metric_rect.y), 18, int(metric_rect.height)) + rl.draw_rectangle_rounded(edge_rect, 0.18, 10, metric.color) + rl.end_scissor_mode() + + # Draw border + rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.15, 10, 2, Colors.METRIC_BORDER) + + # Draw text + text = f"{metric.label}\n{metric.value}" + text_size = measure_text_cached(self._font_bold, text, 35) + text_pos = rl.Vector2( + metric_rect.x + 22 + (metric_rect.width - 22 - text_size.x) / 2, + metric_rect.y + (metric_rect.height - text_size.y) / 2 + ) + rl.draw_text_ex(self._font_bold, text, text_pos, 35, 0, Colors.WHITE) diff --git a/selfdrive/ui/lib/prime_state.py b/selfdrive/ui/lib/prime_state.py new file mode 100644 index 0000000000..6b87540f65 --- /dev/null +++ b/selfdrive/ui/lib/prime_state.py @@ -0,0 +1,99 @@ +from enum import IntEnum +import os +import threading +import time + +from openpilot.common.api import Api, api_get +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog +from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID + + +class PrimeType(IntEnum): + UNKNOWN = -2, + UNPAIRED = -1, + NONE = 0, + MAGENTA = 1, + LITE = 2, + BLUE = 3, + MAGENTA_NEW = 4, + PURPLE = 5, + + +class PrimeState: + FETCH_INTERVAL = 5.0 # seconds between API calls + API_TIMEOUT = 10.0 # seconds for API requests + SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread + + def __init__(self): + self._params = Params() + self._lock = threading.Lock() + self.prime_type: PrimeType = self._load_initial_state() + + self._running = False + self._thread = None + self.start() + + def _load_initial_state(self) -> PrimeType: + prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType", encoding='utf8') + try: + if prime_type_str is not None: + return PrimeType(int(prime_type_str)) + except (ValueError, TypeError): + pass + return PrimeType.UNKNOWN + + def _fetch_prime_status(self) -> None: + dongle_id = self._params.get("DongleId", encoding='utf8') + if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: + return + + try: + identity_token = Api(dongle_id).get_token() + response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token) + if response.status_code == 200: + data = response.json() + is_paired = data.get("is_paired", False) + prime_type = data.get("prime_type", 0) + self.set_type(PrimeType(prime_type) if is_paired else PrimeType.UNPAIRED) + except Exception as e: + cloudlog.error(f"Failed to fetch prime status: {e}") + + def set_type(self, prime_type: PrimeType) -> None: + with self._lock: + if prime_type != self.prime_type: + self.prime_type = prime_type + self._params.put("PrimeType", str(int(prime_type))) + cloudlog.info(f"Prime type updated to {prime_type}") + + def _worker_thread(self) -> None: + while self._running: + self._fetch_prime_status() + + for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)): + if not self._running: + break + time.sleep(self.SLEEP_INTERVAL) + + def start(self) -> None: + if self._thread and self._thread.is_alive(): + return + self._running = True + self._thread = threading.Thread(target=self._worker_thread, daemon=True) + self._thread.start() + + def stop(self) -> None: + self._running = False + if self._thread and self._thread.is_alive(): + self._thread.join(timeout=1.0) + + def get_type(self) -> PrimeType: + with self._lock: + return self.prime_type + + def is_prime(self) -> bool: + with self._lock: + return bool(self.prime_type > PrimeType.NONE) + + def __del__(self): + self.stop() diff --git a/selfdrive/ui/onroad/__init__.py b/selfdrive/ui/onroad/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/selfdrive/ui/onroad/alert_renderer.py b/selfdrive/ui/onroad/alert_renderer.py new file mode 100644 index 0000000000..7a17291bbe --- /dev/null +++ b/selfdrive/ui/onroad/alert_renderer.py @@ -0,0 +1,162 @@ +import time +import pyray as rl +from dataclasses import dataclass +from cereal import messaging, log +from openpilot.system.hardware import TICI +from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS +from openpilot.system.ui.lib.label import gui_text_box +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.widget import Widget +from openpilot.selfdrive.ui.ui_state import ui_state + +ALERT_MARGIN = 40 +ALERT_PADDING = 60 +ALERT_LINE_SPACING = 45 +ALERT_BORDER_RADIUS = 30 + +ALERT_FONT_SMALL = 66 +ALERT_FONT_MEDIUM = 74 +ALERT_FONT_BIG = 88 + +SELFDRIVE_STATE_TIMEOUT = 5 # Seconds +SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds + +# Constants +ALERT_COLORS = { + log.SelfdriveState.AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black + log.SelfdriveState.AlertStatus.userPrompt: rl.Color(0xFE, 0x8C, 0x34, 235), # Orange + log.SelfdriveState.AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 235), # Red +} + + +@dataclass +class Alert: + text1: str = "" + text2: str = "" + size: int = 0 + status: int = 0 + + +# Pre-defined alert instances +ALERT_STARTUP_PENDING = Alert( + text1="openpilot Unavailable", + text2="Waiting to start", + size=log.SelfdriveState.AlertSize.mid, + status=log.SelfdriveState.AlertStatus.normal, +) + +ALERT_CRITICAL_TIMEOUT = Alert( + text1="TAKE CONTROL IMMEDIATELY", + text2="System Unresponsive", + size=log.SelfdriveState.AlertSize.full, + status=log.SelfdriveState.AlertStatus.critical, +) + +ALERT_CRITICAL_REBOOT = Alert( + text1="System Unresponsive", + text2="Reboot Device", + size=log.SelfdriveState.AlertSize.full, + status=log.SelfdriveState.AlertStatus.critical, +) + + +class AlertRenderer(Widget): + def __init__(self): + super().__init__() + self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL) + self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD) + + def get_alert(self, sm: messaging.SubMaster) -> Alert | None: + """Generate the current alert based on selfdrive state.""" + ss = sm['selfdriveState'] + + # Check if selfdriveState messages have stopped arriving + if not sm.updated['selfdriveState']: + recv_frame = sm.recv_frame['selfdriveState'] + time_since_onroad = (sm.frame - ui_state.started_frame) / DEFAULT_FPS + + # 1. Never received selfdriveState since going onroad + waiting_for_startup = recv_frame < ui_state.started_frame + if waiting_for_startup and time_since_onroad > 5: + return ALERT_STARTUP_PENDING + + # 2. Lost communication with selfdriveState after receiving it + if TICI and not waiting_for_startup: + ss_missing = time.monotonic() - sm.recv_time['selfdriveState'] + if ss_missing > SELFDRIVE_STATE_TIMEOUT: + if ss.enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < SELFDRIVE_UNRESPONSIVE_TIMEOUT: + return ALERT_CRITICAL_TIMEOUT + return ALERT_CRITICAL_REBOOT + + # No alert if size is none + if ss.alertSize == 0: + return None + + # Return current alert + return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus) + + def _render(self, rect: rl.Rectangle) -> bool: + alert = self.get_alert(ui_state.sm) + if not alert: + return False + + alert_rect = self._get_alert_rect(rect, alert.size) + self._draw_background(alert_rect, alert) + + text_rect = rl.Rectangle( + alert_rect.x + ALERT_PADDING, + alert_rect.y + ALERT_PADDING, + alert_rect.width - 2 * ALERT_PADDING, + alert_rect.height - 2 * ALERT_PADDING + ) + self._draw_text(text_rect, alert) + return True + + def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle: + if size == log.SelfdriveState.AlertSize.full: + return rect + + height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == log.SelfdriveState.AlertSize.small else + ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING) + + return rl.Rectangle( + rect.x + ALERT_MARGIN, + rect.y + rect.height - ALERT_MARGIN - height, + rect.width - 2 * ALERT_MARGIN, + height + ) + + def _draw_background(self, rect: rl.Rectangle, alert: Alert) -> None: + color = ALERT_COLORS.get(alert.status, ALERT_COLORS[log.SelfdriveState.AlertStatus.normal]) + + if alert.size != log.SelfdriveState.AlertSize.full: + roundness = ALERT_BORDER_RADIUS / (min(rect.width, rect.height) / 2) + rl.draw_rectangle_rounded(rect, roundness, 10, color) + else: + rl.draw_rectangle_rec(rect, color) + + def _draw_text(self, rect: rl.Rectangle, alert: Alert) -> None: + if alert.size == log.SelfdriveState.AlertSize.small: + self._draw_centered(alert.text1, rect, self.font_bold, ALERT_FONT_MEDIUM) + + elif alert.size == log.SelfdriveState.AlertSize.mid: + self._draw_centered(alert.text1, rect, self.font_bold, ALERT_FONT_BIG, center_y=False) + rect.y += ALERT_FONT_BIG + ALERT_LINE_SPACING + self._draw_centered(alert.text2, rect, self.font_regular, ALERT_FONT_SMALL, center_y=False) + + else: + is_long = len(alert.text1) > 15 + font_size1 = 132 if is_long else 177 + align_ment = rl.GuiTextAlignment.TEXT_ALIGN_CENTER + vertical_align = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE + text_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height // 2) + + gui_text_box(text_rect, alert.text1, font_size1, alignment=align_ment, alignment_vertical=vertical_align, font_weight=FontWeight.BOLD) + text_rect.y = rect.y + rect.height // 2 + gui_text_box(text_rect, alert.text2, ALERT_FONT_BIG, alignment=align_ment) + + def _draw_centered(self, text, rect, font, font_size, center_y=True, color=rl.WHITE) -> None: + text_size = measure_text_cached(font, text, font_size) + x = rect.x + (rect.width - text_size.x) / 2 + y = rect.y + ((rect.height - text_size.y) / 2 if center_y else 0) + rl.draw_text_ex(font, text, rl.Vector2(x, y), font_size, 0, color) diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py new file mode 100644 index 0000000000..dbc1919221 --- /dev/null +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -0,0 +1,227 @@ +import numpy as np +import pyray as rl +from collections.abc import Callable +from cereal import log +from msgq.visionipc import VisionStreamType +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, UI_BORDER_SIZE +from openpilot.selfdrive.ui.onroad.alert_renderer import AlertRenderer +from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer +from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer +from openpilot.selfdrive.ui.onroad.model_renderer import ModelRenderer +from openpilot.selfdrive.ui.onroad.cameraview import CameraView +from openpilot.system.ui.lib.application import gui_app +from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame +from openpilot.common.transformations.orientation import rot_from_euler + +OpState = log.SelfdriveState.OpenpilotState +CALIBRATED = log.LiveCalibrationData.Status.calibrated +ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD +WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD +DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"] + +BORDER_COLORS = { + UIStatus.DISENGAGED: rl.Color(0x17, 0x33, 0x49, 0xC8), # Blue for disengaged state + UIStatus.OVERRIDE: rl.Color(0x91, 0x9B, 0x95, 0xF1), # Gray for override state + UIStatus.ENGAGED: rl.Color(0x17, 0x86, 0x44, 0xF1), # Green for engaged state +} + +WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph) +ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph) + + +class AugmentedRoadView(CameraView): + def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD): + super().__init__("camerad", stream_type) + self._set_placeholder_color(BORDER_COLORS[UIStatus.DISENGAGED]) + + self.device_camera: DeviceCameraConfig | None = None + self.view_from_calib = view_frame_from_device_frame.copy() + self.view_from_wide_calib = view_frame_from_device_frame.copy() + + self._last_calib_time: float = 0 + self._last_rect_dims = (0.0, 0.0) + self._last_stream_type = stream_type + self._cached_matrix: np.ndarray | None = None + self._content_rect = rl.Rectangle() + + self.model_renderer = ModelRenderer() + self._hud_renderer = HudRenderer() + self.alert_renderer = AlertRenderer() + self.driver_state_renderer = DriverStateRenderer() + + # Callbacks + self._click_callback: Callable | None = None + + def set_callbacks(self, on_click: Callable | None = None): + self._click_callback = on_click + + def _render(self, rect): + # Only render when system is started to avoid invalid data access + if not ui_state.started: + return + + self._switch_stream_if_needed(ui_state.sm) + + # Update calibration before rendering + self._update_calibration() + + # Create inner content area with border padding + self._content_rect = rl.Rectangle( + rect.x + UI_BORDER_SIZE, + rect.y + UI_BORDER_SIZE, + rect.width - 2 * UI_BORDER_SIZE, + rect.height - 2 * UI_BORDER_SIZE, + ) + + # Draw colored border based on driving state + self._draw_border(rect) + + # Enable scissor mode to clip all rendering within content rectangle boundaries + # This creates a rendering viewport that prevents graphics from drawing outside the border + rl.begin_scissor_mode( + int(self._content_rect.x), + int(self._content_rect.y), + int(self._content_rect.width), + int(self._content_rect.height) + ) + + # Render the base camera view + super()._render(rect) + + # Draw all UI overlays + self.model_renderer.render(self._content_rect) + self._hud_renderer.render(self._content_rect) + if not self.alert_renderer.render(self._content_rect): + self.driver_state_renderer.render(self._content_rect) + + # Custom UI extension point - add custom overlays here + # Use self._content_rect for positioning within camera bounds + + # End clipping region + rl.end_scissor_mode() + + # Handle click events if no HUD interaction occurred + if not self._hud_renderer.handle_mouse_event(): + if self._click_callback and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): + if rl.check_collision_point_rec(rl.get_mouse_position(), self._content_rect): + self._click_callback() + + def _draw_border(self, rect: rl.Rectangle): + border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED]) + rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, border_color) + + def _switch_stream_if_needed(self, sm): + if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams: + v_ego = sm['carState'].vEgo + if v_ego < WIDE_CAM_MAX_SPEED: + target = WIDE_CAM + elif v_ego > ROAD_CAM_MIN_SPEED: + target = ROAD_CAM + else: + # Hysteresis zone - keep current stream + target = self.stream_type + else: + target = ROAD_CAM + + if self.stream_type != target: + self.switch_stream(target) + + def _update_calibration(self): + # Update device camera if not already set + sm = ui_state.sm + if not self.device_camera and sm.seen['roadCameraState'] and sm.seen['deviceState']: + self.device_camera = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] + + # Check if live calibration data is available and valid + if not (sm.updated["liveCalibration"] and sm.valid['liveCalibration']): + return + + calib = sm['liveCalibration'] + if len(calib.rpyCalib) != 3 or calib.calStatus != CALIBRATED: + return + + # Update view_from_calib matrix + device_from_calib = rot_from_euler(calib.rpyCalib) + self.view_from_calib = view_frame_from_device_frame @ device_from_calib + + # Update wide calibration if available + if hasattr(calib, 'wideFromDeviceEuler') and len(calib.wideFromDeviceEuler) == 3: + wide_from_device = rot_from_euler(calib.wideFromDeviceEuler) + self.view_from_wide_calib = view_frame_from_device_frame @ wide_from_device @ device_from_calib + + def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: + # Check if we can use cached matrix + calib_time = ui_state.sm.recv_frame['liveCalibration'] + current_dims = (self._content_rect.width, self._content_rect.height) + if (self._last_calib_time == calib_time and + self._last_rect_dims == current_dims and + self._last_stream_type == self.stream_type and + self._cached_matrix is not None): + return self._cached_matrix + + # Get camera configuration + device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA + is_wide_camera = self.stream_type == WIDE_CAM + intrinsic = device_camera.ecam.intrinsics if is_wide_camera else device_camera.fcam.intrinsics + calibration = self.view_from_wide_calib if is_wide_camera else self.view_from_calib + zoom = 2.0 if is_wide_camera else 1.1 + + # Calculate transforms for vanishing point + inf_point = np.array([1000.0, 0.0, 0.0]) + calib_transform = intrinsic @ calibration + kep = calib_transform @ inf_point + + # Calculate center points and dimensions + x, y = self._content_rect.x, self._content_rect.y + w, h = self._content_rect.width, self._content_rect.height + cx, cy = intrinsic[0, 2], intrinsic[1, 2] + + # Calculate max allowed offsets with margins + margin = 5 + max_x_offset = cx * zoom - w / 2 - margin + max_y_offset = cy * zoom - h / 2 - margin + + # Calculate and clamp offsets to prevent out-of-bounds issues + try: + if abs(kep[2]) > 1e-6: + x_offset = np.clip((kep[0] / kep[2] - cx) * zoom, -max_x_offset, max_x_offset) + y_offset = np.clip((kep[1] / kep[2] - cy) * zoom, -max_y_offset, max_y_offset) + else: + x_offset, y_offset = 0, 0 + except (ZeroDivisionError, OverflowError): + x_offset, y_offset = 0, 0 + + # Cache the computed transformation matrix to avoid recalculations + self._last_calib_time = calib_time + self._last_rect_dims = current_dims + self._last_stream_type = self.stream_type + self._cached_matrix = np.array([ + [zoom * 2 * cx / w, 0, -x_offset / w * 2], + [0, zoom * 2 * cy / h, -y_offset / h * 2], + [0, 0, 1.0] + ]) + + video_transform = np.array([ + [zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)], + [0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)], + [0.0, 0.0, 1.0] + ]) + self.model_renderer.set_transform(video_transform @ calib_transform) + + return self._cached_matrix + + +if __name__ == "__main__": + gui_app.init_window("OnRoad Camera View") + road_camera_view = AugmentedRoadView(ROAD_CAM) + print("***press space to switch camera view***") + try: + for _ in gui_app.render(): + ui_state.update() + if rl.is_key_released(rl.KeyboardKey.KEY_SPACE): + if WIDE_CAM in road_camera_view.available_streams: + stream = ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM + road_camera_view.switch_stream(stream) + road_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + finally: + road_camera_view.close() diff --git a/system/ui/widgets/cameraview.py b/selfdrive/ui/onroad/cameraview.py similarity index 53% rename from system/ui/widgets/cameraview.py rename to selfdrive/ui/onroad/cameraview.py index 01aac0370b..33f1961d2b 100644 --- a/system/ui/widgets/cameraview.py +++ b/selfdrive/ui/onroad/cameraview.py @@ -1,12 +1,27 @@ +import platform +import numpy as np import pyray as rl + from openpilot.system.hardware import TICI from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf +from openpilot.common.swaglog import cloudlog from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage +from openpilot.system.ui.lib.widget import Widget +CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts -VERTEX_SHADER = """ +VERSION = """ #version 300 es +precision mediump float; +""" +if platform.system() == "Darwin": + VERSION = """ + #version 330 core + """ + + +VERTEX_SHADER = VERSION + """ in vec3 vertexPosition; in vec2 vertexTexCoord; in vec3 vertexNormal; @@ -36,9 +51,7 @@ if TICI: } """ else: - FRAME_FRAGMENT_SHADER = """ - #version 300 es - precision mediump float; + FRAME_FRAGMENT_SHADER = VERSION + """ in vec2 fragTexCoord; uniform sampler2D texture0; uniform sampler2D texture1; @@ -50,10 +63,25 @@ else: } """ -class CameraView: + +class CameraView(Widget): def __init__(self, name: str, stream_type: VisionStreamType): - self.client = VisionIpcClient(name, stream_type, False) + super().__init__() + self._name = name + # Primary stream + self.client = VisionIpcClient(name, stream_type, conflate=True) + self._stream_type = stream_type + self.available_streams: list[VisionStreamType] = [] + + # Target stream for switching + self._target_client: VisionIpcClient | None = None + self._target_stream_type: VisionStreamType | None = None + self._switching: bool = False + + self._texture_needs_update = True + self.last_connection_attempt: float = 0.0 self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER) + self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not TICI else -1 self.frame: VisionBuf | None = None self.texture_y: rl.Texture | None = None @@ -63,6 +91,8 @@ class CameraView: self.egl_images: dict[int, EGLImage] = {} self.egl_texture: rl.Texture | None = None + self._placeholder_color: rl.Color | None = None + # Initialize EGL for zero-copy rendering on TICI if TICI: if not init_egl(): @@ -73,6 +103,30 @@ class CameraView: self.egl_texture = rl.load_texture_from_image(temp_image) rl.unload_image(temp_image) + def _set_placeholder_color(self, color: rl.Color): + """Set a placeholder color to be drawn when no frame is available.""" + self._placeholder_color = color + + def switch_stream(self, stream_type: VisionStreamType) -> None: + if self._stream_type == stream_type: + return + + if self._switching and self._target_stream_type == stream_type: + return + + cloudlog.debug(f'Preparing switch from {self._stream_type} to {stream_type}') + + if self._target_client: + del self._target_client + + self._target_stream_type = stream_type + self._target_client = VisionIpcClient(self._name, stream_type, conflate=True) + self._switching = True + + @property + def stream_type(self) -> VisionStreamType: + return self._stream_type + def close(self) -> None: self._clear_textures() @@ -85,24 +139,59 @@ class CameraView: if self.shader and self.shader.id: rl.unload_shader(self.shader) - def render(self, rect: rl.Rectangle): + self.client = None + + def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: + if not self.frame: + return np.eye(3) + + # Calculate aspect ratios + widget_aspect_ratio = rect.width / rect.height + frame_aspect_ratio = self.frame.width / self.frame.height + + # Calculate scaling factors to maintain aspect ratio + zx = min(frame_aspect_ratio / widget_aspect_ratio, 1.0) + zy = min(widget_aspect_ratio / frame_aspect_ratio, 1.0) + + return np.array([ + [zx, 0.0, 0.0], + [0.0, zy, 0.0], + [0.0, 0.0, 1.0] + ]) + + def _render(self, rect: rl.Rectangle): + if self._switching: + self._handle_switch() + if not self._ensure_connection(): + self._draw_placeholder(rect) return # Try to get a new buffer without blocking buffer = self.client.recv(timeout_ms=0) if buffer: + self._texture_needs_update = True self.frame = buffer if not self.frame: + self._draw_placeholder(rect) return - # Calculate scaling to maintain aspect ratio - scale = min(rect.width / self.frame.width, rect.height / self.frame.height) - x_offset = rect.x + (rect.width - (self.frame.width * scale)) / 2 - y_offset = rect.y + (rect.height - (self.frame.height * scale)) / 2 + transform = self._calc_frame_matrix(rect) src_rect = rl.Rectangle(0, 0, float(self.frame.width), float(self.frame.height)) - dst_rect = rl.Rectangle(x_offset, y_offset, self.frame.width * scale, self.frame.height * scale) + + # Calculate scale + scale_x = rect.width * transform[0, 0] # zx + scale_y = rect.height * transform[1, 1] # zy + + # Calculate base position (centered) + x_offset = rect.x + (rect.width - scale_x) / 2 + y_offset = rect.y + (rect.height - scale_y) / 2 + + x_offset += transform[0, 2] * rect.width / 2 + y_offset += transform[1, 2] * rect.height / 2 + + dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y) # Render with appropriate method if TICI: @@ -110,6 +199,10 @@ class CameraView: else: self._render_textures(src_rect, dst_rect) + def _draw_placeholder(self, rect: rl.Rectangle): + if self._placeholder_color: + rl.draw_rectangle_rec(rect, self._placeholder_color) + def _render_egl(self, src_rect: rl.Rectangle, dst_rect: rl.Rectangle) -> None: """Render using EGL for direct buffer access""" if self.frame is None or self.egl_texture is None: @@ -144,34 +237,86 @@ class CameraView: return # Update textures with new frame data - y_data = self.frame.data[: self.frame.uv_offset] - uv_data = self.frame.data[self.frame.uv_offset :] + if self._texture_needs_update: + y_data = self.frame.data[: self.frame.uv_offset] + uv_data = self.frame.data[self.frame.uv_offset:] - rl.update_texture(self.texture_y, rl.ffi.cast("void *", y_data.ctypes.data)) - rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data)) + rl.update_texture(self.texture_y, rl.ffi.cast("void *", y_data.ctypes.data)) + rl.update_texture(self.texture_uv, rl.ffi.cast("void *", uv_data.ctypes.data)) + self._texture_needs_update = False # Render with shader rl.begin_shader_mode(self.shader) - rl.set_shader_value_texture(self.shader, rl.get_shader_location(self.shader, "texture1"), self.texture_uv) + rl.set_shader_value_texture(self.shader, self._texture1_loc, self.texture_uv) rl.draw_texture_pro(self.texture_y, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE) rl.end_shader_mode() def _ensure_connection(self) -> bool: if not self.client.is_connected(): self.frame = None + self.available_streams.clear() + + # Throttle connection attempts + current_time = rl.get_time() + if current_time - self.last_connection_attempt < CONNECTION_RETRY_INTERVAL: + return False + self.last_connection_attempt = current_time + if not self.client.connect(False) or not self.client.num_buffers: return False - self._clear_textures() + cloudlog.debug(f"Connected to {self._name} stream: {self._stream_type}, buffers: {self.client.num_buffers}") + self._initialize_textures() + self.available_streams = self.client.available_streams(self._name, block=False) + return True + + def _handle_switch(self) -> None: + """Check if target stream is ready and switch immediately.""" + if not self._target_client or not self._switching: + return + + # Try to connect target if needed + if not self._target_client.is_connected(): + if not self._target_client.connect(False) or not self._target_client.num_buffers: + return + + cloudlog.debug(f"Target stream connected: {self._target_stream_type}") + + # Check if target has frames ready + target_frame = self._target_client.recv(timeout_ms=0) + if target_frame: + self.frame = target_frame # Update current frame to target frame + self._complete_switch() + + def _complete_switch(self) -> None: + """Instantly switch to target stream.""" + cloudlog.debug(f"Switching to {self._target_stream_type}") + # Clean up current resources + if self.client: + del self.client + + # Switch to target + self.client = self._target_client + self._stream_type = self._target_stream_type + self._texture_needs_update = True + + # Reset state + self._target_client = None + self._target_stream_type = None + self._switching = False + + # Initialize textures for new stream + self._initialize_textures() + + def _initialize_textures(self): + self._clear_textures() if not TICI: self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride), int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE)) self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2), int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA)) - return True - def _clear_textures(self): if self.texture_y and self.texture_y.id: rl.unload_texture(self.texture_y) diff --git a/selfdrive/ui/onroad/driver_camera_dialog.py b/selfdrive/ui/onroad/driver_camera_dialog.py new file mode 100644 index 0000000000..40c72f292b --- /dev/null +++ b/selfdrive/ui/onroad/driver_camera_dialog.py @@ -0,0 +1,98 @@ +import numpy as np +import pyray as rl +from msgq.visionipc import VisionStreamType +from openpilot.selfdrive.ui.onroad.cameraview import CameraView +from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.label import gui_label + + +class DriverCameraDialog(CameraView): + def __init__(self): + super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER) + self.driver_state_renderer = DriverStateRenderer() + + def _render(self, rect): + super()._render(rect) + + if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): + return 1 + + if not self.frame: + gui_label( + rect, + "camera starting", + font_size=100, + font_weight=FontWeight.BOLD, + alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + ) + return -1 + + self._draw_face_detection(rect) + self.driver_state_renderer.render(rect) + + return -1 + + def _draw_face_detection(self, rect: rl.Rectangle) -> None: + driver_state = ui_state.sm["driverStateV2"] + is_rhd = driver_state.wheelOnRightProb > 0.5 + driver_data = driver_state.rightDriverData if is_rhd else driver_state.leftDriverData + face_detect = driver_data.faceProb > 0.7 + if not face_detect: + return + + # Get face position and orientation + face_x, face_y = driver_data.facePosition + face_std = max(driver_data.faceOrientationStd[0], driver_data.faceOrientationStd[1]) + alpha = 0.7 + if face_std > 0.15: + alpha = max(0.7 - (face_std - 0.15) * 3.5, 0.0) + + # use approx instead of distort_points + # TODO: replace with distort_points + fbox_x = int(1080.0 - 1714.0 * face_x) + fbox_y = int(-135.0 + (504.0 + abs(face_x) * 112.0) + (1205.0 - abs(face_x) * 724.0) * face_y) + box_size = 220 + + line_color = rl.Color(255, 255, 255, int(alpha * 255)) + rl.draw_rectangle_rounded_lines_ex( + rl.Rectangle(fbox_x - box_size / 2, fbox_y - box_size / 2, box_size, box_size), + 35.0 / box_size / 2, + 10, + 10, + line_color, + ) + + def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: + driver_view_ratio = 2.0 + + # Get stream dimensions + if self.frame: + stream_width = self.frame.width + stream_height = self.frame.height + else: + # Default values if frame not available + stream_width = 1928 + stream_height = 1208 + + yscale = stream_height * driver_view_ratio / stream_width + xscale = yscale * rect.height / rect.width * stream_width / stream_height + + return np.array([ + [xscale, 0.0, 0.0], + [0.0, yscale, 0.0], + [0.0, 0.0, 1.0] + ]) + + +if __name__ == "__main__": + gui_app.init_window("Driver Camera View") + + driver_camera_view = DriverCameraDialog() + try: + for _ in gui_app.render(): + ui_state.update() + driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + finally: + driver_camera_view.close() diff --git a/selfdrive/ui/onroad/driver_state.py b/selfdrive/ui/onroad/driver_state.py new file mode 100644 index 0000000000..8d5fb951d0 --- /dev/null +++ b/selfdrive/ui/onroad/driver_state.py @@ -0,0 +1,232 @@ +import numpy as np +import pyray as rl +from dataclasses import dataclass +from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.widget import Widget + +# Default 3D coordinates for face keypoints as a NumPy array +DEFAULT_FACE_KPTS_3D = np.array([ + [-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00], + [-32.04, -37.49, 8.00], [-34.10, -32.00, 8.00], [-36.16, -21.03, 8.00], [-36.16, 6.40, 8.00], + [-35.47, 10.51, 8.00], [-32.73, 19.43, 8.00], [-29.30, 26.29, 8.00], [-24.50, 33.83, 8.00], + [-19.01, 41.37, 8.00], [-14.21, 46.17, 8.00], [-12.16, 47.54, 8.00], [-4.61, 49.60, 8.00], + [4.99, 49.60, 8.00], [12.53, 47.54, 8.00], [14.59, 46.17, 8.00], [19.39, 41.37, 8.00], + [24.87, 33.83, 8.00], [29.67, 26.29, 8.00], [33.10, 19.43, 8.00], [35.84, 10.51, 8.00], + [36.53, 6.40, 8.00], [36.53, -21.03, 8.00], [34.47, -32.00, 8.00], [32.42, -37.49, 8.00], + [30.36, -40.91, 8.00], [24.19, -46.40, 8.00], [18.02, -49.14, 8.00], [6.36, -51.20, 8.00], + [-5.98, -51.20, 8.00], +], dtype=np.float32) + +# UI constants +BTN_SIZE = 192 +IMG_SIZE = 144 +ARC_LENGTH = 133 +ARC_THICKNESS_DEFAULT = 6.7 +ARC_THICKNESS_EXTEND = 12.0 + +SCALES_POS = np.array([0.9, 0.4, 0.4], dtype=np.float32) +SCALES_NEG = np.array([0.7, 0.4, 0.4], dtype=np.float32) + +ARC_POINT_COUNT = 37 # Number of points in the arc +ARC_ANGLES = np.linspace(0.0, np.pi, ARC_POINT_COUNT, dtype=np.float32) + + +@dataclass +class ArcData: + """Data structure for arc rendering parameters.""" + x: float + y: float + width: float + height: float + thickness: float + + +class DriverStateRenderer(Widget): + def __init__(self): + super().__init__() + # Initial state with NumPy arrays + self.face_kpts_draw = DEFAULT_FACE_KPTS_3D.copy() + self.is_active = False + self.is_rhd = False + self.dm_fade_state = 0.0 + self.last_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) + self.driver_pose_vals = np.zeros(3, dtype=np.float32) + self.driver_pose_diff = np.zeros(3, dtype=np.float32) + self.driver_pose_sins = np.zeros(3, dtype=np.float32) + self.driver_pose_coss = np.zeros(3, dtype=np.float32) + self.face_keypoints_transformed = np.zeros((DEFAULT_FACE_KPTS_3D.shape[0], 2), dtype=np.float32) + self.position_x: float = 0.0 + self.position_y: float = 0.0 + self.h_arc_data = None + self.v_arc_data = None + + # Pre-allocate drawing arrays + self.face_lines = [rl.Vector2(0, 0) for _ in range(len(DEFAULT_FACE_KPTS_3D))] + self.h_arc_lines = [rl.Vector2(0, 0) for _ in range(ARC_POINT_COUNT)] + self.v_arc_lines = [rl.Vector2(0, 0) for _ in range(ARC_POINT_COUNT)] + + # Load the driver face icon + self.dm_img = gui_app.texture("icons/driver_face.png", IMG_SIZE, IMG_SIZE) + + # Colors + self.white_color = rl.Color(255, 255, 255, 255) + self.arc_color = rl.Color(26, 242, 66, 255) + self.engaged_color = rl.Color(26, 242, 66, 255) + self.disengaged_color = rl.Color(139, 139, 139, 255) + + self.set_visible(lambda: (ui_state.sm.recv_frame['driverStateV2'] > ui_state.started_frame and + ui_state.sm.seen['driverMonitoringState'])) + + def _render(self, rect): + # Set opacity based on active state + opacity = 0.65 if self.is_active else 0.2 + + # Draw background circle + rl.draw_circle(int(self.position_x), int(self.position_y), BTN_SIZE // 2, rl.Color(0, 0, 0, 70)) + + # Draw face icon + icon_pos = rl.Vector2(self.position_x - self.dm_img.width // 2, self.position_y - self.dm_img.height // 2) + rl.draw_texture_v(self.dm_img, icon_pos, rl.Color(255, 255, 255, int(255 * opacity))) + + # Draw face outline + self.white_color.a = int(255 * opacity) + rl.draw_spline_linear(self.face_lines, len(self.face_lines), 5.2, self.white_color) + + # Set arc color based on engaged state + self.arc_color = self.engaged_color if ui_state.engaged else self.disengaged_color + self.arc_color.a = int(0.4 * 255 * (1.0 - self.dm_fade_state)) # Fade out when inactive + + # Draw arcs + if self.h_arc_data: + rl.draw_spline_linear(self.h_arc_lines, len(self.h_arc_lines), self.h_arc_data.thickness, self.arc_color) + if self.v_arc_data: + rl.draw_spline_linear(self.v_arc_lines, len(self.v_arc_lines), self.v_arc_data.thickness, self.arc_color) + + def _update_state(self): + """Update the driver monitoring state based on model data""" + sm = ui_state.sm + if not sm.updated["driverMonitoringState"]: + if (self._rect.x != self.last_rect.x or self._rect.y != self.last_rect.y or + self._rect.width != self.last_rect.width or self._rect.height != self.last_rect.height): + self._pre_calculate_drawing_elements() + self.last_rect = self._rect + return + + # Get monitoring state + dm_state = sm["driverMonitoringState"] + self.is_active = dm_state.isActiveMode + self.is_rhd = dm_state.isRHD + + # Update fade state (smoother transition between active/inactive) + fade_target = 0.0 if self.is_active else 0.5 + self.dm_fade_state = np.clip(self.dm_fade_state + 0.2 * (fade_target - self.dm_fade_state), 0.0, 1.0) + + # Get driver orientation data from appropriate camera + driverstate = sm["driverStateV2"] + driver_data = driverstate.rightDriverData if self.is_rhd else driverstate.leftDriverData + driver_orient = driver_data.faceOrientation + + # Update pose values with scaling and smoothing + driver_orient = np.array(driver_orient) + scales = np.where(driver_orient < 0, SCALES_NEG, SCALES_POS) + v_this = driver_orient * scales + self.driver_pose_diff = np.abs(self.driver_pose_vals - v_this) + self.driver_pose_vals = 0.8 * v_this + 0.2 * self.driver_pose_vals # Smooth changes + + # Apply fade to rotation and compute sin/cos + rotation_amount = self.driver_pose_vals * (1.0 - self.dm_fade_state) + self.driver_pose_sins = np.sin(rotation_amount) + self.driver_pose_coss = np.cos(rotation_amount) + + # Create rotation matrix for 3D face model + sin_y, sin_x, sin_z = self.driver_pose_sins + cos_y, cos_x, cos_z = self.driver_pose_coss + r_xyz = np.array( + [ + [cos_x * cos_z, cos_x * sin_z, -sin_x], + [-sin_y * sin_x * cos_z - cos_y * sin_z, -sin_y * sin_x * sin_z + cos_y * cos_z, -sin_y * cos_x], + [cos_y * sin_x * cos_z - sin_y * sin_z, cos_y * sin_x * sin_z + sin_y * cos_z, cos_y * cos_x], + ] + ) + + # Transform face keypoints using vectorized matrix multiplication + self.face_kpts_draw = DEFAULT_FACE_KPTS_3D @ r_xyz.T + self.face_kpts_draw[:, 2] = self.face_kpts_draw[:, 2] * (1.0 - self.dm_fade_state) + 8 * self.dm_fade_state + + # Pre-calculate the transformed keypoints + kp_depth = (self.face_kpts_draw[:, 2] - 8) / 120.0 + 1.0 + self.face_keypoints_transformed = self.face_kpts_draw[:, :2] * kp_depth[:, None] + + # Pre-calculate all drawing elements + self._pre_calculate_drawing_elements() + + def _pre_calculate_drawing_elements(self): + """Pre-calculate all drawing elements based on the current rectangle""" + # Calculate icon position (bottom-left or bottom-right) + width, height = self._rect.width, self._rect.height + offset = UI_BORDER_SIZE + BTN_SIZE // 2 + self.position_x = self._rect.x + (width - offset if self.is_rhd else offset) + self.position_y = self._rect.y + height - offset + + # Pre-calculate the face lines positions + positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y]) + for i in range(len(positioned_keypoints)): + self.face_lines[i].x = positioned_keypoints[i][0] + self.face_lines[i].y = positioned_keypoints[i][1] + + # Calculate arc dimensions based on head rotation + delta_x = -self.driver_pose_sins[1] * ARC_LENGTH / 2.0 # Horizontal movement + delta_y = -self.driver_pose_sins[0] * ARC_LENGTH / 2.0 # Vertical movement + + # Horizontal arc + h_width = abs(delta_x) + self.h_arc_data = self._calculate_arc_data( + delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2, + self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True + ) + + # Vertical arc + v_height = abs(delta_y) + self.v_arc_data = self._calculate_arc_data( + delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y, + self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False + ) + + def _calculate_arc_data( + self, delta: float, size: float, x: float, y: float, sin_val: float, diff_val: float, is_horizontal: bool + ): + """Calculate arc data and pre-compute arc points.""" + if size <= 0: + return None + + thickness = ARC_THICKNESS_DEFAULT + ARC_THICKNESS_EXTEND * min(1.0, diff_val * 5.0) + start_angle = (90 if sin_val > 0 else -90) if is_horizontal else (0 if sin_val > 0 else 180) + x = min(x + delta, x) if is_horizontal else x + y = y if is_horizontal else min(y + delta, y) + + arc_data = ArcData( + x=x, + y=y, + width=size if is_horizontal else ARC_LENGTH, + height=ARC_LENGTH if is_horizontal else size, + thickness=thickness, + ) + + # Pre-calculate arc points + angles = ARC_ANGLES + np.deg2rad(start_angle) + + center_x = x + arc_data.width / 2 + center_y = y + arc_data.height / 2 + radius_x = arc_data.width / 2 + radius_y = arc_data.height / 2 + + x_coords = center_x + np.cos(angles) * radius_x + y_coords = center_y + np.sin(angles) * radius_y + + arc_lines = self.h_arc_lines if is_horizontal else self.v_arc_lines + for i, (x_coord, y_coord) in enumerate(zip(x_coords, y_coords, strict=True)): + arc_lines[i].x = x_coord + arc_lines[i].y = y_coord + + return arc_data diff --git a/selfdrive/ui/onroad/exp_button.py b/selfdrive/ui/onroad/exp_button.py new file mode 100644 index 0000000000..8c835090d0 --- /dev/null +++ b/selfdrive/ui/onroad/exp_button.py @@ -0,0 +1,78 @@ +import time +import pyray as rl +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.widget import Widget +from openpilot.common.params import Params + + +class ExpButton(Widget): + def __init__(self, button_size: int, icon_size: int): + super().__init__() + self._params = Params() + self._experimental_mode: bool = False + self._engageable: bool = False + + # State hold mechanism + self._hold_duration = 2.0 # seconds + self._held_mode: bool | None = None + self._hold_end_time: float | None = None + + self._white_color: rl.Color = rl.Color(255, 255, 255, 255) + self._black_bg: rl.Color = rl.Color(0, 0, 0, 166) + self._txt_wheel: rl.Texture = gui_app.texture('icons/chffr_wheel.png', icon_size, icon_size) + self._txt_exp: rl.Texture = gui_app.texture('icons/experimental.png', icon_size, icon_size) + self._rect = rl.Rectangle(0, 0, button_size, button_size) + + def set_rect(self, rect: rl.Rectangle) -> None: + self._rect.x, self._rect.y = rect.x, rect.y + + def _update_state(self) -> None: + selfdrive_state = ui_state.sm["selfdriveState"] + self._experimental_mode = selfdrive_state.experimentalMode + self._engageable = selfdrive_state.engageable or selfdrive_state.enabled + + def handle_mouse_event(self) -> bool: + if rl.check_collision_point_rec(rl.get_mouse_position(), self._rect): + if (rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and + self._is_toggle_allowed()): + new_mode = not self._experimental_mode + self._params.put_bool("ExperimentalMode", new_mode) + + # Hold new state temporarily + self._held_mode = new_mode + self._hold_end_time = time.time() + self._hold_duration + return True + return False + + def _render(self, rect: rl.Rectangle) -> None: + center_x = int(self._rect.x + self._rect.width // 2) + 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 + 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 + rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg) + rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color) + + def _held_or_actual_mode(self): + now = time.time() + if self._hold_end_time and now < self._hold_end_time: + return self._held_mode + + if self._hold_end_time and now >= self._hold_end_time: + self._hold_end_time = self._held_mode = None + + return self._experimental_mode + + def _is_toggle_allowed(self): + if not self._params.get_bool("ExperimentalModeConfirmed"): + return False + + car_params = ui_state.sm["carParams"] + if car_params.alphaLongitudinalAvailable: + return self._params.get_bool("AlphaLongitudinalEnabled") + else: + return car_params.openpilotLongitudinalControl diff --git a/selfdrive/ui/onroad/hud_renderer.py b/selfdrive/ui/onroad/hud_renderer.py new file mode 100644 index 0000000000..da292d38d5 --- /dev/null +++ b/selfdrive/ui/onroad/hud_renderer.py @@ -0,0 +1,179 @@ +import pyray as rl +from dataclasses import dataclass +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus +from openpilot.selfdrive.ui.onroad.exp_button import ExpButton +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.common.conversions import Conversions as CV +from openpilot.system.ui.lib.widget import Widget + +# Constants +SET_SPEED_NA = 255 +KM_TO_MILE = 0.621371 +CRUISE_DISABLED_CHAR = '–' + + +@dataclass(frozen=True) +class UIConfig: + header_height: int = 300 + border_size: int = 30 + button_size: int = 192 + set_speed_width_metric: int = 200 + set_speed_width_imperial: int = 172 + set_speed_height: int = 204 + wheel_icon_size: int = 144 + + +@dataclass(frozen=True) +class FontSizes: + current_speed: int = 176 + speed_unit: int = 66 + max_speed: int = 40 + set_speed: int = 90 + + +@dataclass(frozen=True) +class Colors: + white: rl.Color = rl.Color(255, 255, 255, 255) + disengaged: rl.Color = rl.Color(145, 155, 149, 255) + override: rl.Color = rl.Color(145, 155, 149, 255) # Added + engaged: rl.Color = rl.Color(128, 216, 166, 255) + disengaged_bg: rl.Color = rl.Color(0, 0, 0, 153) + override_bg: rl.Color = rl.Color(145, 155, 149, 204) + engaged_bg: rl.Color = rl.Color(128, 216, 166, 204) + grey: rl.Color = rl.Color(166, 166, 166, 255) + dark_grey: rl.Color = rl.Color(114, 114, 114, 255) + black_translucent: rl.Color = rl.Color(0, 0, 0, 166) + white_translucent: rl.Color = rl.Color(255, 255, 255, 200) + border_translucent: rl.Color = rl.Color(255, 255, 255, 75) + header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114) + header_gradient_end: rl.Color = rl.Color(0, 0, 0, 0) + + +UI_CONFIG = UIConfig() +FONT_SIZES = FontSizes() +COLORS = Colors() + + +class HudRenderer(Widget): + def __init__(self): + super().__init__() + """Initialize the HUD renderer.""" + self.is_cruise_set: bool = False + self.is_cruise_available: bool = False + self.set_speed: float = SET_SPEED_NA + self.speed: float = 0.0 + self.v_ego_cluster_seen: bool = False + + self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD) + self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD) + self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM) + + self._exp_button = ExpButton(UI_CONFIG.button_size, UI_CONFIG.wheel_icon_size) + + def _update_state(self) -> None: + """Update HUD state based on car state and controls state.""" + sm = ui_state.sm + if sm.recv_frame["carState"] < ui_state.started_frame: + self.is_cruise_set = False + self.set_speed = SET_SPEED_NA + self.speed = 0.0 + return + + controls_state = sm['controlsState'] + car_state = sm['carState'] + + v_cruise_cluster = car_state.vCruiseCluster + self.set_speed = ( + controls_state.vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster + ) + self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA + self.is_cruise_available = self.set_speed != -1 + + if self.is_cruise_set and not ui_state.is_metric: + self.set_speed *= KM_TO_MILE + + v_ego_cluster = car_state.vEgoCluster + self.v_ego_cluster_seen = self.v_ego_cluster_seen or v_ego_cluster != 0.0 + v_ego = v_ego_cluster if self.v_ego_cluster_seen else car_state.vEgo + speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH + self.speed = max(0.0, v_ego * speed_conversion) + + def _render(self, rect: rl.Rectangle) -> None: + """Render HUD elements to the screen.""" + # Draw the header background + rl.draw_rectangle_gradient_v( + int(rect.x), + int(rect.y), + int(rect.width), + UI_CONFIG.header_height, + COLORS.header_gradient_start, + COLORS.header_gradient_end, + ) + + if self.is_cruise_available: + self._draw_set_speed(rect) + + self._draw_current_speed(rect) + + button_x = rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size + button_y = rect.y + UI_CONFIG.border_size + self._exp_button.render(rl.Rectangle(button_x, button_y, UI_CONFIG.button_size, UI_CONFIG.button_size)) + + def handle_mouse_event(self) -> bool: + return bool(self._exp_button.handle_mouse_event()) + + def _draw_set_speed(self, rect: rl.Rectangle) -> None: + """Draw the MAX speed indicator box.""" + set_speed_width = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial + x = rect.x + 60 + (UI_CONFIG.set_speed_width_imperial - set_speed_width) // 2 + y = rect.y + 45 + + set_speed_rect = rl.Rectangle(x, y, set_speed_width, UI_CONFIG.set_speed_height) + rl.draw_rectangle_rounded(set_speed_rect, 0.2, 30, COLORS.black_translucent) + rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.2, 30, 6, COLORS.border_translucent) + + max_color = COLORS.grey + set_speed_color = COLORS.dark_grey + if self.is_cruise_set: + set_speed_color = COLORS.white + if ui_state.status == UIStatus.ENGAGED: + max_color = COLORS.engaged + elif ui_state.status == UIStatus.DISENGAGED: + max_color = COLORS.disengaged + elif ui_state.status == UIStatus.OVERRIDE: + max_color = COLORS.override + + max_text = "MAX" + max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x + rl.draw_text_ex( + self._font_semi_bold, + max_text, + rl.Vector2(x + (set_speed_width - max_text_width) / 2, y + 27), + FONT_SIZES.max_speed, + 0, + max_color, + ) + + set_speed_text = CRUISE_DISABLED_CHAR if not self.is_cruise_set else str(round(self.set_speed)) + speed_text_width = measure_text_cached(self._font_bold, set_speed_text, FONT_SIZES.set_speed).x + rl.draw_text_ex( + self._font_bold, + set_speed_text, + rl.Vector2(x + (set_speed_width - speed_text_width) / 2, y + 77), + FONT_SIZES.set_speed, + 0, + set_speed_color, + ) + + def _draw_current_speed(self, rect: rl.Rectangle) -> None: + """Draw the current vehicle speed and unit.""" + speed_text = str(round(self.speed)) + speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed) + speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2) + rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white) + + unit_text = "km/h" if ui_state.is_metric else "mph" + unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit) + unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2) + rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent) diff --git a/selfdrive/ui/onroad/model_renderer.py b/selfdrive/ui/onroad/model_renderer.py new file mode 100644 index 0000000000..ab2b7aaefb --- /dev/null +++ b/selfdrive/ui/onroad/model_renderer.py @@ -0,0 +1,442 @@ +import colorsys +import numpy as np +import pyray as rl +from cereal import messaging, car +from dataclasses import dataclass, field +from openpilot.common.params import Params +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import DEFAULT_FPS +from openpilot.system.ui.lib.shader_polygon import draw_polygon +from openpilot.system.ui.lib.widget import Widget +from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT + +CLIP_MARGIN = 500 +MIN_DRAW_DISTANCE = 10.0 +MAX_DRAW_DISTANCE = 100.0 +PATH_COLOR_TRANSITION_DURATION = 0.5 # Seconds for color transition animation +PATH_BLEND_INCREMENT = 1.0 / (PATH_COLOR_TRANSITION_DURATION * DEFAULT_FPS) + +MAX_POINTS = 200 + +THROTTLE_COLORS = [ + rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4) + rl.Color(114, 255, 92, 89), # HSLF(112/360, 1.0, 0.68, 0.35) + rl.Color(114, 255, 92, 0), # HSLF(112/360, 1.0, 0.68, 0.0) +] + +NO_THROTTLE_COLORS = [ + rl.Color(242, 242, 242, 102), # HSLF(148/360, 0.0, 0.95, 0.4) + rl.Color(242, 242, 242, 89), # HSLF(112/360, 0.0, 0.95, 0.35) + rl.Color(242, 242, 242, 0), # HSLF(112/360, 0.0, 0.95, 0.0) +] + + +@dataclass +class ModelPoints: + raw_points: np.ndarray = field(default_factory=lambda: np.empty((0, 3), dtype=np.float32)) + projected_points: np.ndarray = field(default_factory=lambda: np.empty((0, 2), dtype=np.float32)) + + +@dataclass +class LeadVehicle: + glow: list[float] = field(default_factory=list) + chevron: list[float] = field(default_factory=list) + fill_alpha: int = 0 + + +class ModelRenderer(Widget): + def __init__(self): + super().__init__() + self._longitudinal_control = False + self._experimental_mode = False + self._blend_factor = 1.0 + self._prev_allow_throttle = True + self._lane_line_probs = np.zeros(4, dtype=np.float32) + self._road_edge_stds = np.zeros(2, dtype=np.float32) + self._lead_vehicles = [LeadVehicle(), LeadVehicle()] + self._path_offset_z = HEIGHT_INIT[0] + + # Initialize ModelPoints objects + self._path = ModelPoints() + self._lane_lines = [ModelPoints() for _ in range(4)] + self._road_edges = [ModelPoints() for _ in range(2)] + self._acceleration_x = np.empty((0,), dtype=np.float32) + + # Transform matrix (3x3 for car space to screen space) + self._car_space_transform = np.zeros((3, 3), dtype=np.float32) + self._transform_dirty = True + self._clip_region = None + + self._exp_gradient = { + 'start': (0.0, 1.0), # Bottom of path + 'end': (0.0, 0.0), # Top of path + 'colors': [], + 'stops': [], + } + + # Get longitudinal control setting from car parameters + if car_params := Params().get("CarParams"): + cp = messaging.log_from_bytes(car_params, car.CarParams) + self._longitudinal_control = cp.openpilotLongitudinalControl + + def set_transform(self, transform: np.ndarray): + self._car_space_transform = transform.astype(np.float32) + self._transform_dirty = True + + def _render(self, rect: rl.Rectangle): + sm = ui_state.sm + + # Check if data is up-to-date + if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or + sm.recv_frame["modelV2"] < ui_state.started_frame): + return + + # Set up clipping region + self._clip_region = rl.Rectangle( + rect.x - CLIP_MARGIN, rect.y - CLIP_MARGIN, rect.width + 2 * CLIP_MARGIN, rect.height + 2 * CLIP_MARGIN + ) + + # Update state + self._experimental_mode = sm['selfdriveState'].experimentalMode + + live_calib = sm['liveCalibration'] + self._path_offset_z = live_calib.height[0] if live_calib.height else HEIGHT_INIT[0] + + if sm.updated['carParams']: + self._longitudinal_control = sm['carParams'].openpilotLongitudinalControl + + model = sm['modelV2'] + radar_state = sm['radarState'] if sm.valid['radarState'] else None + lead_one = radar_state.leadOne if radar_state else None + render_lead_indicator = self._longitudinal_control and radar_state is not None + + # Update model data when needed + model_updated = sm.updated['modelV2'] + if model_updated or sm.updated['radarState'] or self._transform_dirty: + if model_updated: + self._update_raw_points(model) + + path_x_array = self._path.raw_points[:, 0] + if path_x_array.size == 0: + return + + self._update_model(lead_one, path_x_array) + if render_lead_indicator: + self._update_leads(radar_state, path_x_array) + self._transform_dirty = False + + # Draw elements + self._draw_lane_lines() + self._draw_path(sm) + + if render_lead_indicator and radar_state: + self._draw_lead_indicator() + + def _update_raw_points(self, model): + """Update raw 3D points from model data""" + self._path.raw_points = np.array([model.position.x, model.position.y, model.position.z], dtype=np.float32).T + + for i, lane_line in enumerate(model.laneLines): + self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, lane_line.z], dtype=np.float32).T + + for i, road_edge in enumerate(model.roadEdges): + self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, road_edge.z], dtype=np.float32).T + + self._lane_line_probs = np.array(model.laneLineProbs, dtype=np.float32) + self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32) + self._acceleration_x = np.array(model.acceleration.x, dtype=np.float32) + + def _update_leads(self, radar_state, path_x_array): + """Update positions of lead vehicles""" + self._lead_vehicles = [LeadVehicle(), LeadVehicle()] + leads = [radar_state.leadOne, radar_state.leadTwo] + + for i, lead_data in enumerate(leads): + if lead_data and lead_data.status: + d_rel, y_rel, v_rel = lead_data.dRel, lead_data.yRel, lead_data.vRel + idx = self._get_path_length_idx(path_x_array, d_rel) + + # Get z-coordinate from path at the lead vehicle position + z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0 + point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z) + if point: + self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect) + + def _update_model(self, lead, path_x_array): + """Update model visualization data based on model message""" + max_distance = np.clip(path_x_array[-1], MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE) + max_idx = self._get_path_length_idx(self._lane_lines[0].raw_points[:, 0], max_distance) + + # Update lane lines using raw points + for i, lane_line in enumerate(self._lane_lines): + lane_line.projected_points = self._map_line_to_polygon( + lane_line.raw_points, 0.025 * self._lane_line_probs[i], 0.0, max_idx + ) + + # Update road edges using raw points + for road_edge in self._road_edges: + road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx) + + # Update path using raw points + if lead and lead.status: + lead_d = lead.dRel * 2.0 + max_distance = np.clip(lead_d - min(lead_d * 0.35, 10.0), 0.0, max_distance) + + max_idx = self._get_path_length_idx(path_x_array, max_distance) + self._path.projected_points = self._map_line_to_polygon( + self._path.raw_points, 0.9, self._path_offset_z, max_idx, allow_invert=False + ) + + self._update_experimental_gradient(self._rect.height) + + def _update_experimental_gradient(self, height): + """Pre-calculate experimental mode gradient colors""" + if not self._experimental_mode: + return + + max_len = min(len(self._path.projected_points) // 2, len(self._acceleration_x)) + + segment_colors = [] + gradient_stops = [] + + i = 0 + while i < max_len: + track_idx = max_len - i - 1 # flip idx to start from bottom right + track_y = self._path.projected_points[track_idx][1] + if track_y < 0 or track_y > height: + i += 1 + continue + + # Calculate color based on acceleration + lin_grad_point = (height - track_y) / height + + # speed up: 120, slow down: 0 + path_hue = max(min(60 + self._acceleration_x[i] * 35, 120), 0) + path_hue = int(path_hue * 100 + 0.5) / 100 + + saturation = min(abs(self._acceleration_x[i] * 1.5), 1) + lightness = self._map_val(saturation, 0.0, 1.0, 0.95, 0.62) + alpha = self._map_val(lin_grad_point, 0.75 / 2.0, 0.75, 0.4, 0.0) + + # Use HSL to RGB conversion + color = self._hsla_to_color(path_hue / 360.0, saturation, lightness, alpha) + + gradient_stops.append(lin_grad_point) + segment_colors.append(color) + + # Skip a point, unless next is last + i += 1 + (1 if (i + 2) < max_len else 0) + + # Store the gradient in the path object + self._exp_gradient['colors'] = segment_colors + self._exp_gradient['stops'] = gradient_stops + + def _update_lead_vehicle(self, d_rel, v_rel, point, rect): + speed_buff, lead_buff = 10.0, 40.0 + + # Calculate fill alpha + fill_alpha = 0 + if d_rel < lead_buff: + fill_alpha = 255 * (1.0 - (d_rel / lead_buff)) + if v_rel < 0: + fill_alpha += 255 * (-1 * (v_rel / speed_buff)) + fill_alpha = min(fill_alpha, 255) + + # Calculate size and position + sz = np.clip((25 * 30) / (d_rel / 3 + 30), 15.0, 30.0) * 2.35 + x = np.clip(point[0], 0.0, rect.width - sz / 2) + y = min(point[1], rect.height - sz * 0.6) + + g_xo = sz / 5 + g_yo = sz / 10 + + glow = [(x + (sz * 1.35) + g_xo, y + sz + g_yo), (x, y - g_yo), (x - (sz * 1.35) - g_xo, y + sz + g_yo)] + chevron = [(x + (sz * 1.25), y + sz), (x, y), (x - (sz * 1.25), y + sz)] + + return LeadVehicle(glow=glow, chevron=chevron, fill_alpha=int(fill_alpha)) + + def _draw_lane_lines(self): + """Draw lane lines and road edges""" + for i, lane_line in enumerate(self._lane_lines): + if lane_line.projected_points.size == 0: + continue + + alpha = np.clip(self._lane_line_probs[i], 0.0, 0.7) + color = rl.Color(255, 255, 255, int(alpha * 255)) + draw_polygon(self._rect, lane_line.projected_points, color) + + for i, road_edge in enumerate(self._road_edges): + if road_edge.projected_points.size == 0: + continue + + alpha = np.clip(1.0 - self._road_edge_stds[i], 0.0, 1.0) + color = rl.Color(255, 0, 0, int(alpha * 255)) + draw_polygon(self._rect, road_edge.projected_points, color) + + def _draw_path(self, sm): + """Draw path with dynamic coloring based on mode and throttle state.""" + if not self._path.projected_points.size: + return + + if self._experimental_mode: + # Draw with acceleration coloring + if len(self._exp_gradient['colors']) > 2: + draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient) + else: + draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30)) + else: + # Draw with throttle/no throttle gradient + allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control + + # Start transition if throttle state changes + if allow_throttle != self._prev_allow_throttle: + self._prev_allow_throttle = allow_throttle + self._blend_factor = max(1.0 - self._blend_factor, 0.0) + + # Update blend factor + if self._blend_factor < 1.0: + self._blend_factor = min(self._blend_factor + PATH_BLEND_INCREMENT, 1.0) + + begin_colors = NO_THROTTLE_COLORS if allow_throttle else THROTTLE_COLORS + end_colors = THROTTLE_COLORS if allow_throttle else NO_THROTTLE_COLORS + + # Blend colors based on transition + blended_colors = self._blend_colors(begin_colors, end_colors, self._blend_factor) + gradient = { + 'start': (0.0, 1.0), # Bottom of path + 'end': (0.0, 0.0), # Top of path + 'colors': blended_colors, + 'stops': [0.0, 0.5, 1.0], + } + draw_polygon(self._rect, self._path.projected_points, gradient=gradient) + + def _draw_lead_indicator(self): + # Draw lead vehicles if available + for lead in self._lead_vehicles: + if not lead.glow or not lead.chevron: + continue + + rl.draw_triangle_fan(lead.glow, len(lead.glow), rl.Color(218, 202, 37, 255)) + rl.draw_triangle_fan(lead.chevron, len(lead.chevron), rl.Color(201, 34, 49, lead.fill_alpha)) + + @staticmethod + def _get_path_length_idx(pos_x_array: np.ndarray, path_height: float) -> int: + """Get the index corresponding to the given path height""" + if len(pos_x_array) == 0: + return 0 + indices = np.where(pos_x_array <= path_height)[0] + return indices[-1] if indices.size > 0 else 0 + + def _map_to_screen(self, in_x, in_y, in_z): + """Project a point in car space to screen space""" + input_pt = np.array([in_x, in_y, in_z]) + pt = self._car_space_transform @ input_pt + + if abs(pt[2]) < 1e-6: + return None + + x, y = pt[0] / pt[2], pt[1] / pt[2] + + clip = self._clip_region + if not (clip.x <= x <= clip.x + clip.width and clip.y <= y <= clip.y + clip.height): + return None + + return (x, y) + + def _map_line_to_polygon(self, line: np.ndarray, y_off: float, z_off: float, max_idx: int, allow_invert: bool = True) -> np.ndarray: + """Convert 3D line to 2D polygon for rendering.""" + if line.shape[0] == 0: + return np.empty((0, 2), dtype=np.float32) + + # Slice points and filter non-negative x-coordinates + points = line[:max_idx + 1] + points = points[points[:, 0] >= 0] + if points.shape[0] == 0: + return np.empty((0, 2), dtype=np.float32) + + N = points.shape[0] + # Generate left and right 3D points in one array using broadcasting + offsets = np.array([[0, -y_off, z_off], [0, y_off, z_off]], dtype=np.float32) + points_3d = points[None, :, :] + offsets[:, None, :] # Shape: 2xNx3 + points_3d = points_3d.reshape(2 * N, 3) # Shape: (2*N)x3 + + # Transform all points to projected space in one operation + proj = self._car_space_transform @ points_3d.T # Shape: 3x(2*N) + proj = proj.reshape(3, 2, N) + left_proj = proj[:, 0, :] + right_proj = proj[:, 1, :] + + # Filter points where z is sufficiently large + valid_proj = (np.abs(left_proj[2]) >= 1e-6) & (np.abs(right_proj[2]) >= 1e-6) + if not np.any(valid_proj): + return np.empty((0, 2), dtype=np.float32) + + # Compute screen coordinates + left_screen = left_proj[:2, valid_proj] / left_proj[2, valid_proj][None, :] + right_screen = right_proj[:2, valid_proj] / right_proj[2, valid_proj][None, :] + + # Define clip region bounds + clip = self._clip_region + x_min, x_max = clip.x, clip.x + clip.width + y_min, y_max = clip.y, clip.y + clip.height + + # Filter points within clip region + left_in_clip = ( + (left_screen[0] >= x_min) & (left_screen[0] <= x_max) & + (left_screen[1] >= y_min) & (left_screen[1] <= y_max) + ) + right_in_clip = ( + (right_screen[0] >= x_min) & (right_screen[0] <= x_max) & + (right_screen[1] >= y_min) & (right_screen[1] <= y_max) + ) + both_in_clip = left_in_clip & right_in_clip + + if not np.any(both_in_clip): + return np.empty((0, 2), dtype=np.float32) + + # Select valid and clipped points + left_screen = left_screen[:, both_in_clip] + right_screen = right_screen[:, both_in_clip] + + # Handle Y-coordinate inversion on hills + if not allow_invert and left_screen.shape[1] > 1: + y = left_screen[1, :] # y-coordinates + keep = y == np.minimum.accumulate(y) + if not np.any(keep): + return np.empty((0, 2), dtype=np.float32) + left_screen = left_screen[:, keep] + right_screen = right_screen[:, keep] + + return np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32) + + @staticmethod + def _map_val(x, x0, x1, y0, y1): + x = np.clip(x, x0, x1) + ra = x1 - x0 + rb = y1 - y0 + return (x - x0) * rb / ra + y0 if ra != 0 else y0 + + @staticmethod + def _hsla_to_color(h, s, l, a): + rgb = colorsys.hls_to_rgb(h, l, s) + return rl.Color( + int(rgb[0] * 255), + int(rgb[1] * 255), + int(rgb[2] * 255), + int(a * 255) + ) + + @staticmethod + def _blend_colors(begin_colors, end_colors, t): + if t >= 1.0: + return end_colors + if t <= 0.0: + return begin_colors + + inv_t = 1.0 - t + return [rl.Color( + int(inv_t * start.r + t * end.r), + int(inv_t * start.g + t * end.g), + int(inv_t * start.b + t * end.b), + int(inv_t * start.a + t * end.a) + ) for start, end in zip(begin_colors, end_colors, strict=True)] diff --git a/selfdrive/ui/qt/network/networking.cc b/selfdrive/ui/qt/network/networking.cc index 7ace68ef94..7754e6e5ba 100644 --- a/selfdrive/ui/qt/network/networking.cc +++ b/selfdrive/ui/qt/network/networking.cc @@ -79,7 +79,8 @@ Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) { } void Networking::setPrimeType(PrimeState::Type type) { - an->setGsmVisible(type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_LITE); + an->setGsmVisible(type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_UNKNOWN || \ + type == PrimeState::PRIME_TYPE_PURPLE || type == PrimeState::PRIME_TYPE_LITE); wifi->ipv4_forward = (type == PrimeState::PRIME_TYPE_NONE || type == PrimeState::PRIME_TYPE_LITE); } @@ -190,7 +191,7 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid // Wi-Fi metered toggle std::vector metered_button_texts{tr("default"), tr("metered"), tr("unmetered")}; - wifiMeteredToggle = new MultiButtonControl(tr("Wi-Fi Network Metered"), tr("Prevent large data uploads when on a metered Wi-FI connection"), "", metered_button_texts); + wifiMeteredToggle = new MultiButtonControl(tr("Wi-Fi Network Metered"), tr("Prevent large data uploads when on a metered Wi-Fi connection"), "", metered_button_texts); QObject::connect(wifiMeteredToggle, &MultiButtonControl::buttonClicked, [=](int id) { wifiMeteredToggle->setEnabled(false); MeteredType metered = MeteredType::UNKNOWN; diff --git a/selfdrive/ui/qt/offroad/firehose.cc b/selfdrive/ui/qt/offroad/firehose.cc index c710a8bf28..17df440c36 100644 --- a/selfdrive/ui/qt/offroad/firehose.cc +++ b/selfdrive/ui/qt/offroad/firehose.cc @@ -22,7 +22,7 @@ FirehosePanel::FirehosePanel(SettingsWindow *parent) : QWidget((QWidget*)parent) layout->setSpacing(20); // header - QLabel *title = new QLabel(tr("🔥 Firehose Mode 🔥")); + QLabel *title = new QLabel(tr("Firehose Mode")); title->setStyleSheet("font-size: 100px; font-weight: 500; font-family: 'Noto Color Emoji';"); layout->addWidget(title, 0, Qt::AlignCenter); diff --git a/selfdrive/ui/qt/offroad/offroad_home.cc b/selfdrive/ui/qt/offroad/offroad_home.cc index 6fcb6a71ae..9ca41e5567 100644 --- a/selfdrive/ui/qt/offroad/offroad_home.cc +++ b/selfdrive/ui/qt/offroad/offroad_home.cc @@ -18,7 +18,7 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { main_layout->setContentsMargins(40, 40, 40, 40); // top header - QHBoxLayout* header_layout = new QHBoxLayout(); + header_layout = new QHBoxLayout(); header_layout->setContentsMargins(0, 0, 0, 0); header_layout->setSpacing(16); diff --git a/selfdrive/ui/qt/offroad/offroad_home.h b/selfdrive/ui/qt/offroad/offroad_home.h index 40b63ce8d8..cac37d58cd 100644 --- a/selfdrive/ui/qt/offroad/offroad_home.h +++ b/selfdrive/ui/qt/offroad/offroad_home.h @@ -39,11 +39,13 @@ public: protected: QHBoxLayout *home_layout; + QHBoxLayout *header_layout; + + void showEvent(QShowEvent *event) override; + void refresh(); private: - void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override; - void refresh(); Params params; diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 9fc3a2edf1..6b97342966 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -17,55 +17,63 @@ #include "selfdrive/ui/qt/offroad/firehose.h" TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { - // param, title, desc, icon - std::vector> toggle_defs{ + // param, title, desc, icon, restart needed + std::vector> toggle_defs{ { "OpenpilotEnabledToggle", tr("Enable sunnypilot"), - tr("Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off."), + tr("Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature."), "../assets/icons/chffr_wheel.png", + true, }, { "ExperimentalMode", tr("Experimental Mode"), "", "../assets/icons/experimental_white.svg", + false, }, { "DynamicExperimentalControl", tr("Enable Dynamic Experimental Control"), tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."), "../assets/offroad/icon_blank.png", + false, }, { "DisengageOnAccelerator", tr("Disengage on Accelerator Pedal"), tr("When enabled, pressing the accelerator pedal will disengage sunnypilot."), "../assets/icons/disengage_on_accelerator.svg", + false, }, { "IsLdwEnabled", tr("Enable Lane Departure Warnings"), tr("Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h)."), "../assets/icons/warning.png", + false, }, { "AlwaysOnDM", tr("Always-On Driver Monitoring"), tr("Enable driver monitoring even when sunnypilot is not engaged."), "../assets/icons/monitoring.png", + false, }, { "RecordFront", tr("Record and Upload Driver Camera"), tr("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), "../assets/icons/monitoring.png", + true, }, { "IsMetric", tr("Use Metric System"), tr("Display speed in km/h instead of mph."), "../assets/icons/metric.png", + false, }, }; @@ -81,12 +89,24 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { // set up uiState update for personality setting QObject::connect(uiState(), &UIState::uiUpdate, this, &TogglesPanel::updateState); - for (auto &[param, title, desc, icon] : toggle_defs) { + for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) { auto toggle = new ParamControl(param, title, desc, icon, this); bool locked = params.getBool((param + "Lock").toStdString()); toggle->setEnabled(!locked); + if (needs_restart && !locked) { + toggle->setDescription(toggle->getDescription() + tr(" Changing this setting will restart openpilot if the car is powered on.")); + + QObject::connect(uiState(), &UIState::engagedChanged, [toggle](bool engaged) { + toggle->setEnabled(!engaged); + }); + + QObject::connect(toggle, &ParamControl::toggleFlipped, [=](bool state) { + params.putBool("OnroadCycleRequested", true); + }); + } + addItem(toggle); toggles[param.toStdString()] = toggle; @@ -119,6 +139,15 @@ void TogglesPanel::expandToggleDescription(const QString ¶m) { toggles[param.toStdString()]->showDescription(); } +void TogglesPanel::scrollToToggle(const QString ¶m) { + if (auto it = toggles.find(param.toStdString()); it != toggles.end()) { + auto scroll_area = qobject_cast(parent()->parent()); + if (scroll_area) { + scroll_area->ensureWidgetVisible(it->second); + } + } +} + void TogglesPanel::showEvent(QShowEvent *event) { updateToggles(); } @@ -202,15 +231,24 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { addItem(dcamBtn); #endif - auto resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); + resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription); connect(resetCalibBtn, &ButtonControl::clicked, [&]() { - if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), tr("Reset"), this)) { - params.remove("CalibrationParams"); - params.remove("LiveTorqueParameters"); - params.remove("LiveParameters"); - params.remove("LiveParametersV2"); - params.remove("LiveDelay"); + if (!uiState()->engaged()) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), tr("Reset"), this)) { + // Check engaged again in case it changed while the dialog was open + if (!uiState()->engaged()) { + params.remove("CalibrationParams"); + params.remove("LiveTorqueParameters"); + params.remove("LiveParameters"); + params.remove("LiveParametersV2"); + params.remove("LiveDelay"); + params.putBool("OnroadCycleRequested", true); + updateCalibDescription(); + } + } + } else { + ConfirmationDialog::alert(tr("Disengage to Reset Calibration"), this); } }); addItem(resetCalibBtn); @@ -249,7 +287,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { for (auto btn : findChildren()) { - if (btn != pair_device) { + if (btn != pair_device && btn != resetCalibBtn) { btn->setEnabled(offroad); } } @@ -285,9 +323,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { } void DevicePanel::updateCalibDescription() { - QString desc = - tr("sunnypilot requires the device to be mounted within 4° left or right and " - "within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required."); + QString desc = tr("sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."); std::string calib_bytes = params.get("CalibrationParams"); if (!calib_bytes.empty()) { try { @@ -305,7 +341,53 @@ void DevicePanel::updateCalibDescription() { qInfo() << "invalid CalibrationParams"; } } - qobject_cast(sender())->setDescription(desc); + + const bool is_release = params.getBool("IsReleaseBranch"); + if (!is_release) { + int lag_perc = 0; + std::string lag_bytes = params.get("LiveDelay"); + if (!lag_bytes.empty()) { + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size())); + lag_perc = cmsg.getRoot().getLiveDelay().getCalPerc(); + } catch (kj::Exception) { + qInfo() << "invalid LiveDelay"; + } + } + desc += "\n\n"; + if (lag_perc < 100) { + desc += tr("Steering lag calibration is %1% complete.").arg(lag_perc); + } else { + desc += tr("Steering lag calibration is complete."); + } + } + + std::string torque_bytes = params.get("LiveTorqueParameters"); + if (!torque_bytes.empty()) { + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(torque_bytes.data(), torque_bytes.size())); + auto torque = cmsg.getRoot().getLiveTorqueParameters(); + // don't add for non-torque cars + if (torque.getUseParams()) { + int torque_perc = torque.getCalPerc(); + desc += is_release ? "\n\n" : " "; + if (torque_perc < 100) { + desc += tr("Steering torque response calibration is %1% complete.").arg(torque_perc); + } else { + desc += tr("Steering torque response calibration is complete."); + } + } + } catch (kj::Exception) { + qInfo() << "invalid LiveTorqueParameters"; + } + } + + desc += "\n\n"; + desc += tr("openpilot is continuously calibrating, resetting is rarely required. " + "Resetting calibration will restart openpilot if the car is powered on."); + resetCalibBtn->setDescription(desc); } void DevicePanel::reboot() { @@ -358,6 +440,7 @@ void SettingsWindow::setCurrentPanel(int index, const QString ¶m) { } } else { emit expandToggleDescription(param); + emit scrollToToggle(param); } } @@ -398,6 +481,7 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { TogglesPanel *toggles = new TogglesPanel(this); QObject::connect(this, &SettingsWindow::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); + QObject::connect(this, &SettingsWindow::scrollToToggle, toggles, &TogglesPanel::scrollToToggle); auto networking = new Networking(this); QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &Networking::setPrimeType); diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index 8b162bb843..a11a300128 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -42,6 +42,7 @@ signals: void reviewTrainingGuide(); void showDriverView(); void expandToggleDescription(const QString ¶m); + void scrollToToggle(const QString ¶m); protected: QPushButton *sidebar_alert_widget; @@ -67,6 +68,7 @@ protected slots: protected: Params params; ButtonControl *pair_device; + ButtonControl *resetCalibBtn; }; class TogglesPanel : public ListWidget { @@ -77,6 +79,7 @@ public: public slots: void expandToggleDescription(const QString ¶m); + void scrollToToggle(const QString ¶m); protected slots: virtual void updateState(const UIState &s); diff --git a/selfdrive/ui/qt/onroad/annotated_camera.h b/selfdrive/ui/qt/onroad/annotated_camera.h index 219b39546f..e3ca837907 100644 --- a/selfdrive/ui/qt/onroad/annotated_camera.h +++ b/selfdrive/ui/qt/onroad/annotated_camera.h @@ -9,7 +9,9 @@ #ifdef SUNNYPILOT #include "selfdrive/ui/sunnypilot/qt/onroad/buttons.h" #include "selfdrive/ui/sunnypilot/qt/onroad/hud.h" +#include "selfdrive/ui/sunnypilot/qt/onroad/model.h" #define ExperimentalButton ExperimentalButtonSP +#define ModelRenderer ModelRendererSP #else #include "selfdrive/ui/qt/onroad/buttons.h" #include "selfdrive/ui/qt/onroad/hud.h" diff --git a/selfdrive/ui/qt/onroad/model.cc b/selfdrive/ui/qt/onroad/model.cc index 52902abdc8..dc801e591f 100644 --- a/selfdrive/ui/qt/onroad/model.cc +++ b/selfdrive/ui/qt/onroad/model.cc @@ -1,18 +1,5 @@ #include "selfdrive/ui/qt/onroad/model.h" -constexpr int CLIP_MARGIN = 500; -constexpr float MIN_DRAW_DISTANCE = 10.0; -constexpr float MAX_DRAW_DISTANCE = 100.0; - -static int get_path_length_idx(const cereal::XYZTData::Reader &line, const float path_height) { - const auto &line_x = line.getX(); - int max_idx = 0; - for (int i = 1; i < line_x.size() && line_x[i] <= path_height; ++i) { - max_idx = i; - } - return max_idx; -} - void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) { auto *s = uiState(); auto &sm = *(s->sm); @@ -35,7 +22,7 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) { update_model(model, lead_one); drawLaneLines(painter); - drawPath(painter, model, surface_rect.height()); + drawPath(painter, model, surface_rect); if (longitudinal_control && sm.alive("radarState")) { update_leads(radar_state, model.getPosition()); diff --git a/selfdrive/ui/qt/onroad/model.h b/selfdrive/ui/qt/onroad/model.h index 7e1b43acb4..85eb236e76 100644 --- a/selfdrive/ui/qt/onroad/model.h +++ b/selfdrive/ui/qt/onroad/model.h @@ -9,21 +9,39 @@ #include "selfdrive/ui/ui.h" #endif +constexpr int CLIP_MARGIN = 500; +constexpr float MIN_DRAW_DISTANCE = 10.0; +constexpr float MAX_DRAW_DISTANCE = 100.0; + +inline int get_path_length_idx(const cereal::XYZTData::Reader &line, const float path_height) { + const auto &line_x = line.getX(); + int max_idx = 0; + for (int i = 1; i < line_x.size() && line_x[i] <= path_height; ++i) { + max_idx = i; + } + return max_idx; +} + class ModelRenderer { public: + virtual ~ModelRenderer() = default; + ModelRenderer() {} void setTransform(const Eigen::Matrix3f &transform) { car_space_transform = transform; } void draw(QPainter &painter, const QRect &surface_rect); -private: +protected: bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out); void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off, QPolygonF *pvd, int max_idx, bool allow_invert = true); void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect); void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line); - void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead); + virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead); void drawLaneLines(QPainter &painter); void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, int height); + virtual void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &surface_rect) {; + drawPath(painter, model, surface_rect.height()); + } void updatePathGradient(QLinearGradient &bg); QColor blendColors(const QColor &start, const QColor &end, float t); diff --git a/selfdrive/ui/qt/setup/setup.cc b/selfdrive/ui/qt/setup/setup.cc index 382a15db91..90ed973b0d 100644 --- a/selfdrive/ui/qt/setup/setup.cc +++ b/selfdrive/ui/qt/setup/setup.cc @@ -136,6 +136,59 @@ QWidget * Setup::low_voltage() { 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(); @@ -305,20 +358,17 @@ QWidget * Setup::software_selection() { blayout->addWidget(cont); QObject::connect(cont, &QPushButton::clicked, [=]() { - auto w = currentWidget(); - QTimer::singleShot(0, [=]() { - setCurrentWidget(downloading_widget); - }); - QString url = OPENPILOT_URL; if (group->checkedButton() != openpilot) { - url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software")); - } - if (!url.isEmpty()) { - QTimer::singleShot(1000, this, [=]() { - download(url); + QTimer::singleShot(0, [=]() { + setCurrentWidget(custom_software_warning_widget); }); } else { - setCurrentWidget(w); + QTimer::singleShot(0, [=]() { + setCurrentWidget(downloading_widget); + }); + QTimer::singleShot(1000, this, [=]() { + download(OPENPILOT_URL); + }); } }); @@ -415,8 +465,10 @@ Setup::Setup(QWidget *parent) : QStackedWidget(parent) { addWidget(getting_started()); addWidget(network_setup()); - addWidget(software_selection()); - + 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); diff --git a/selfdrive/ui/qt/setup/setup.h b/selfdrive/ui/qt/setup/setup.h index ee5cc85483..986956c902 100644 --- a/selfdrive/ui/qt/setup/setup.h +++ b/selfdrive/ui/qt/setup/setup.h @@ -15,6 +15,7 @@ public: private: void selectLanguage(); QWidget *low_voltage(); + QWidget *custom_software_warning(); QWidget *getting_started(); QWidget *network_setup(); QWidget *software_selection(); @@ -23,6 +24,8 @@ private: QWidget *failed_widget; QWidget *downloading_widget; + QWidget *custom_software_warning_widget; + QWidget *software_selection_widget; QTranslator translator; signals: diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index d189751843..41cc09141b 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -19,17 +19,22 @@ qt_src = [ "sunnypilot/qt/sidebar.cc", "sunnypilot/qt/window.cc", "sunnypilot/qt/home.cc", + "sunnypilot/qt/offroad/exit_offroad_button.cc", "sunnypilot/qt/offroad/offroad_home.cc", "sunnypilot/qt/offroad/settings/device_panel.cc", "sunnypilot/qt/offroad/settings/lateral_panel.cc", "sunnypilot/qt/offroad/settings/longitudinal_panel.cc", "sunnypilot/qt/offroad/settings/max_time_offroad.cc", + "sunnypilot/qt/offroad/settings/brightness.cc", + "sunnypilot/qt/offroad/settings/models_panel.cc", + "sunnypilot/qt/offroad/settings/osm_panel.cc", "sunnypilot/qt/offroad/settings/settings.cc", "sunnypilot/qt/offroad/settings/software_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink_panel.cc", "sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc", "sunnypilot/qt/offroad/settings/trips_panel.cc", "sunnypilot/qt/offroad/settings/vehicle_panel.cc", + "sunnypilot/qt/offroad/settings/visuals_panel.cc", "sunnypilot/qt/onroad/annotated_camera.cc", "sunnypilot/qt/onroad/buttons.cc", "sunnypilot/qt/onroad/hud.cc", @@ -38,11 +43,16 @@ qt_src = [ ] lateral_panel_qt_src = [ + "sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc", "sunnypilot/qt/offroad/settings/lateral/lane_change_settings.cc", "sunnypilot/qt/offroad/settings/lateral/mads_settings.cc", "sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.cc", ] +longitudinal_panel_qt_src = [ + "sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.cc", +] + network_src = [ "sunnypilot/qt/network/sunnylink/sunnylink_client.cc", "sunnypilot/qt/network/sunnylink/services/base_device_service.cc", @@ -50,6 +60,10 @@ network_src = [ "sunnypilot/qt/network/sunnylink/services/user_service.cc", ] +osm_panel_qt_src = [ + "sunnypilot/qt/offroad/settings/osm/models_fetcher.cc", +] + vehicle_panel_qt_src = [ "sunnypilot/qt/offroad/settings/vehicle/brand_settings_factory.cc", "sunnypilot/qt/offroad/settings/vehicle/brand_settings_interface.cc", @@ -71,8 +85,9 @@ brand_settings_qt_src = [ "sunnypilot/qt/offroad/settings/vehicle/volkswagen_settings.cc", ] + sp_widgets_src = widgets_src + network_src -sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src +sp_qt_src = qt_src + lateral_panel_qt_src + vehicle_panel_qt_src + brand_settings_qt_src + longitudinal_panel_qt_src + osm_panel_qt_src sp_qt_util = qt_util Export('sp_widgets_src', 'sp_qt_src', "sp_qt_util") diff --git a/selfdrive/ui/sunnypilot/qt/common/json_fetcher.h b/selfdrive/ui/sunnypilot/qt/common/json_fetcher.h new file mode 100644 index 0000000000..5691f600a6 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/common/json_fetcher.h @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include +#include +#include + +class JsonFetcher { +public: + static QJsonObject getJsonFromURL(const QString &url) { + const auto qurl = QUrl(url); + QNetworkAccessManager manager; + const QNetworkRequest request(qurl); + QNetworkReply *reply = manager.get(request); + QEventLoop loop; + + // Send GET request + + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "Failed to fetch data from URL: " << reply->errorString(); + return QJsonObject(); + } + + const QByteArray responseData = reply->readAll(); + const QJsonDocument doc = QJsonDocument::fromJson(responseData); + QJsonObject json = doc.object(); + + reply->deleteLater(); + return json; + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.cc b/selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.cc new file mode 100644 index 0000000000..0c7902476a --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.cc @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include + +#include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/widgets/input.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h" + + +ExitOffroadButton::ExitOffroadButton(QWidget *parent) : QPushButton(parent), glowTimer(new QTimer(this)) { + setMouseTracking(true); + + connect(glowTimer, &QTimer::timeout, this, [this]() { + // Pulse alpha up and down + glowAlpha += glowDelta; + if (glowAlpha > 220 || glowAlpha < 10) { + glowDelta *= -1; + } + update(); // trigger repaint + }); + + glowTimer->start(45); + + pixmap = QPixmap("../../sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png").scaledToWidth(img_width, Qt::SmoothTransformation); + + // go to toggles and expand experimental mode description + connect(this, &QPushButton::clicked, [=]() { + if (ConfirmationDialog::confirm(tr("Are you sure you want to exit Always Offroad mode?"), tr("Confirm"), this)) { + params.remove("OffroadMode"); + } + }); + + setFixedHeight(125); + QHBoxLayout *main_layout = new QHBoxLayout; + main_layout->setContentsMargins(horizontal_padding, 0, horizontal_padding, 0); + + mode_label = new QLabel(tr("EXIT ALWAYS OFFROAD MODE")); + mode_icon = new QLabel; + mode_icon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + mode_icon->setPixmap(pixmap); + + main_layout->addWidget(mode_label, 1, Qt::AlignLeft); + main_layout->addWidget(mode_icon, 0, Qt::AlignRight); + + setLayout(main_layout); + + setStyleSheet(R"( + QPushButton { + border: none; + } + + QLabel { + font-size: 45px; + font-weight: 300; + text-align: left; + font-family: JetBrainsMono; + color: #000000; + } + )"); +} + +void drawPulsingGlowOverlay(QPainter &p, QPainterPath path, int glowAlpha) { + // Draw pulsing glow effect clipped to button area + p.save(); + p.setClipPath(path); + p.setCompositionMode(QPainter::CompositionMode_HardLight); + + const QColor animatedGlowColor(255, 255, 255, std::min(255, glowAlpha)); + QPen glowPen(animatedGlowColor, 8); + glowPen.setJoinStyle(Qt::RoundJoin); + p.setPen(glowPen); + p.drawPath(path); + + p.restore(); +} + +void ExitOffroadButton::paintEvent(QPaintEvent *event) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + QPainterPath path; + path.addRoundedRect(rect(), 10, 10); + + // gradient + bool pressed = isDown(); + QLinearGradient gradient(rect().left(), 0, rect().right(), 0); + gradient.setColorAt(0, QColor(35, 149, 255, pressed ? 0xcc : 0xff)); + gradient.setColorAt(0.3, QColor(35, 149, 255, pressed ? 0xcc : 0xff)); + gradient.setColorAt(1, QColor(20, 255, 171, pressed ? 0xcc : 0xff)); + p.fillPath(path, gradient); + + drawPulsingGlowOverlay(p, path, glowAlpha); + + // vertical line + p.setPen(QPen(QColor(0, 0, 0, 0x4d), 3, Qt::SolidLine)); + int line_x = rect().right() - img_width - (2 * horizontal_padding); + p.drawLine(line_x, rect().bottom(), line_x, rect().top()); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h b/selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h new file mode 100644 index 0000000000..673d9fff73 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include "common/params.h" + +class ExitOffroadButton : public QPushButton { + Q_OBJECT + +private: + QTimer *glowTimer; + int glowAlpha = 100; // Current alpha of glow + int glowDelta = 10; // Change per tick + +public: + explicit ExitOffroadButton(QWidget* parent = 0); + + Params params; + bool offroad_mode; + int img_width = 100; + int horizontal_padding = 30; + QPixmap pixmap; + QLabel *mode_label; + QLabel *mode_icon; + +protected: + void paintEvent(QPaintEvent *event) override; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc index 05b1d80846..cc82e6ddb9 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.cc @@ -5,16 +5,45 @@ * See the LICENSE.md file in the root directory for more details. */ -#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h" - #include +#include "selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h" #include "selfdrive/ui/sunnypilot/qt/widgets/drive_stats.h" OffroadHomeSP::OffroadHomeSP(QWidget *parent) : OffroadHome(parent) { - QStackedWidget *left_widget = new QStackedWidget(this); - left_widget->addWidget(new DriveStats(this)); + QFrame *left_widget = new QFrame(this); + QVBoxLayout *left_layout = new QVBoxLayout(left_widget); + left_layout->setContentsMargins(0, 0, 0, 0); + left_layout->setSpacing(30); + + btn_exit_offroad = new ExitOffroadButton(this); + QObject::connect(btn_exit_offroad, &ExitOffroadButton::clicked, [=]() { + refreshOffroadStatus(); + }); + left_layout->addWidget(btn_exit_offroad); + + left_layout->addWidget(new DriveStats(this)); left_widget->setStyleSheet("border-radius: 10px;"); home_layout->insertWidget(0, left_widget); + + offroad_notif = new QPushButton(tr("ALWAYS OFFROAD ACTIVE")); + offroad_notif->setVisible(false); + offroad_notif->setStyleSheet("background-color: #E22C2C;"); + header_layout->insertWidget(0, offroad_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); + + QObject::connect(deviceSP(), &DeviceSP::displayPowerChanged, this, &OffroadHomeSP::refreshOffroadStatus); + +} + +void OffroadHomeSP::showEvent(QShowEvent *event) { + refreshOffroadStatus(); + OffroadHome::showEvent(event); +} + +void OffroadHomeSP::refreshOffroadStatus() { + bool is_offroad = params.getBool("OffroadMode"); + btn_exit_offroad->setVisible(is_offroad); + offroad_notif->setVisible(is_offroad); + OffroadHome::refresh(); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h index 9710150d57..30816f666c 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/offroad_home.h @@ -8,10 +8,19 @@ #pragma once #include "selfdrive/ui/qt/offroad/offroad_home.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/exit_offroad_button.h" class OffroadHomeSP : public OffroadHome { Q_OBJECT public: explicit OffroadHomeSP(QWidget *parent = 0); + +private: + ExitOffroadButton *btn_exit_offroad; + QPushButton *offroad_notif; + Params params; + + void showEvent(QShowEvent *event) override; + void refreshOffroadStatus(); }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.cc new file mode 100644 index 0000000000..ffd3ce1d8b --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.cc @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h" + +// Map of Brightness Options +const QMap Brightness::brightness_options = { + {"0", "1"}, // Auto (Dark) + {"1", "0"}, // Auto + {"2", "10"}, + {"3", "20"}, + {"4", "30"}, + {"5", "40"}, + {"6", "50"}, + {"7", "60"}, + {"8", "70"}, + {"9", "80"}, + {"10", "90"}, + {"11", "100"} +}; + +Brightness::Brightness() : OptionControlSP( + "Brightness", + tr("Brightness"), + tr("Overrides the brightness of the device."), + "../assets/offroad/icon_blank.png", + {0, 11}, 1, true, &brightness_options) { + + refresh(); +} + +void Brightness::refresh() { + const int brightness = QString::fromStdString(params.get("Brightness")).toInt(); + + QString label; + if (brightness == 1) { + label = tr("Auto (Dark)"); + } else if (brightness == 0) { + label = tr("Auto"); + } else { + const int value = brightness; + label = QString("%1").arg(value); + } + + setLabel(label); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h new file mode 100644 index 0000000000..20d0a291d9 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" + +class Brightness : public OptionControlSP { + Q_OBJECT + +public: + static const QMap brightness_options; + + Brightness(); + void refresh(); + +private: + Params params; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index e6855c4b4e..b89116d424 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -80,6 +80,19 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { connect(maxTimeOffroad, &OptionControlSP::updateLabels, maxTimeOffroad, &MaxTimeOffroad::refresh); addItem(maxTimeOffroad); + toggleDeviceBootMode = new ButtonParamControlSP("DeviceBootMode", tr("Wake-Up Behavior"), "", "", {"Default", "Offroad"}, 375, true); + addItem(toggleDeviceBootMode); + + connect(toggleDeviceBootMode, &ButtonParamControlSP::buttonClicked, this, [=](int index) { + params.put("DeviceBootMode", QString::number(index).toStdString()); + updateState(); + }); + + // Brightness + brightness = new Brightness(); + connect(brightness, &OptionControlSP::updateLabels, brightness, &Brightness::refresh); + addItem(brightness); + addItem(device_grid_layout); // offroad mode and power buttons @@ -179,4 +192,10 @@ void DevicePanelSP::updateState() { bool offroad_mode_param = params.getBool("OffroadMode"); offroadBtn->setText(offroad_mode_param ? tr("Exit Always Offroad") : tr("Always Offroad")); offroadBtn->setStyleSheet(offroad_mode_param ? alwaysOffroadStyle : autoOffroadStyle); + + DeviceSleepModeStatus currStatus = DeviceSleepModeStatus::DEFAULT; + if (params.get("DeviceBootMode") == "1") { + currStatus = DeviceSleepModeStatus::OFFROAD; + } + toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus)); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h index 7b7412a739..2cd3281d1f 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h @@ -8,9 +8,15 @@ #pragma once #include "selfdrive/ui/sunnypilot/qt/offroad/settings/max_time_offroad.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/brightness.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +enum class DeviceSleepModeStatus { + DEFAULT, + OFFROAD, +}; + class DevicePanelSP : public DevicePanel { Q_OBJECT @@ -25,6 +31,8 @@ private: std::map buttons; PushButtonSP *offroadBtn; MaxTimeOffroad *maxTimeOffroad; + ButtonParamControlSP *toggleDeviceBootMode; + Brightness *brightness; const QString alwaysOffroadStyle = R"( PushButtonSP { @@ -85,4 +93,20 @@ private: background-color: #FF2424; } )"; + + static QString deviceSleepModeDescription(DeviceSleepModeStatus status = DeviceSleepModeStatus::DEFAULT) { + QString def_str = tr("⁍ Default: Device will boot/wake-up normally & will be ready to engage."); + QString offrd_str = tr("⁍ Offroad: Device will be in Always Offroad mode after boot/wake-up."); + + if (status == DeviceSleepModeStatus::DEFAULT) { + def_str = "" + def_str + ""; + } else if (status == DeviceSleepModeStatus::OFFROAD) { + offrd_str = "" + offrd_str + ""; + } + + return QString("%1

%2
%3") + .arg(tr("Controls state of the device after boot/sleep.")) + .arg(def_str) + .arg(offrd_str); + } }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc new file mode 100644 index 0000000000..b43c6162c6 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.cc @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h" + +BlinkerPauseLateralSettings::BlinkerPauseLateralSettings(const QString ¶m, const QString &title, const QString &description, const QString &icon, QWidget *parent) + : ExpandableToggleRow(param, title, description, icon, parent) { + + pauseLateralSpeed = new OptionControlSP("BlinkerMinLateralControlSpeed", "", "", "", {0, 255}, 5); + connect(pauseLateralSpeed, &OptionControlSP::updateLabels, this, &BlinkerPauseLateralSettings::refresh); + addItem(pauseLateralSpeed); + + refresh(); +} + +void BlinkerPauseLateralSettings::refresh() { + const QString option = QString::fromStdString(params.get("BlinkerMinLateralControlSpeed")); + const bool is_metric = params.getBool("IsMetric"); + const QString unit = is_metric ? "km/h" : "mph"; + + pauseLateralSpeed->setLabel(option + " " + unit); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h new file mode 100644 index 0000000000..7f02337b9c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h" + +class BlinkerPauseLateralSettings : public ExpandableToggleRow { + Q_OBJECT + +public: + BlinkerPauseLateralSettings(const QString ¶m, const QString &title, const QString &description, const QString &icon, QWidget *parent = nullptr); + void refresh(); + +private: + Params params; + OptionControlSP *pauseLateralSpeed; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc index 0beae6e24f..ae954d7539 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.cc @@ -64,6 +64,17 @@ LateralPanel::LateralPanel(SettingsWindowSP *parent) : QFrame(parent) { list->addItem(vertical_space(0)); list->addItem(horizontal_line()); + // Blinker Pause Lateral Control + blinkerPauseLateralSettings = new BlinkerPauseLateralSettings( + "BlinkerPauseLateralControl", + tr("Pause Lateral Control with Blinker"), + tr("Pause lateral control with blinker when traveling below the desired speed selected."), + "", + this); + list->addItem(blinkerPauseLateralSettings); + + list->addItem(horizontal_line()); + // Neural Network Lateral Control nnlcToggle = new NeuralNetworkLateralControl(); list->addItem(nnlcToggle); @@ -139,5 +150,7 @@ void LateralPanel::updateToggles(bool _offroad) { madsSettingsButton->setEnabled(madsToggle->isToggled()); + blinkerPauseLateralSettings->refresh(); + offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h index cf003dfaf3..7b5e73fceb 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h @@ -11,6 +11,7 @@ #include #include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/blinker_pause_lateral_settings.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/mads_settings.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/neural_network_lateral_control.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral/lane_change_settings.h" @@ -43,6 +44,7 @@ private: PushButtonSP *laneChangeSettingsButton; LaneChangeSettings *laneChangeWidget = nullptr; NeuralNetworkLateralControl *nnlcToggle = nullptr; + BlinkerPauseLateralSettings *blinkerPauseLateralSettings = nullptr; const QString MADS_BASE_DESC = tr("Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC)."); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.cc new file mode 100644 index 0000000000..29cadeaa62 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.cc @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h" + +CustomAccIncrement::CustomAccIncrement(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent) + : ExpandableToggleRow(param, title, desc, icon, parent) { + auto *accFrame = new QFrame(this); + auto *accFrameLayout = new QGridLayout(); + accFrame->setLayout(accFrameLayout); + accFrameLayout->setSpacing(0); + + auto *shortPressControl = new AccIncrementOptionControl("CustomAccShortPressIncrement", {1, 10}, 1); + connect(shortPressControl, &OptionControlSP::updateLabels, shortPressControl, &AccIncrementOptionControl::refresh); + + auto *longPressControl = new AccIncrementOptionControl("CustomAccLongPressIncrement", {1, 3}, 1, &customLongValues); + connect(longPressControl, &OptionControlSP::updateLabels, longPressControl, &AccIncrementOptionControl::refresh); + + shortPressControl->setFixedWidth(280); + longPressControl->setFixedWidth(280); + accFrameLayout->addWidget(shortPressControl, 0, 0, Qt::AlignLeft); + accFrameLayout->addWidget(longPressControl, 0, 1, Qt::AlignRight); + + addItem(accFrame); +} + +AccIncrementOptionControl::AccIncrementOptionControl(const QString ¶m, const MinMaxValue &range, const int per_value_change, const QMap *valMap) + : OptionControlSP(param, "", "", "", range, per_value_change, true, valMap) { + param_name = param.toStdString(); + refresh(); +} + +void AccIncrementOptionControl::refresh() { + std::string val = params.get(param_name); + std::string label = ""; + label += param_name == "CustomAccShortPressIncrement" ? "Short Press" : "Long Press"; + label += "
" + val; + label += param_name == "CustomAccShortPressIncrement" + ? (val == "1" ? " (Default)" : "") + : (val == "5" ? " (Default)" : ""); + label += "
"; + setLabel(QString::fromStdString(label)); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h new file mode 100644 index 0000000000..707d4143bc --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/expandable_row.h" + +class CustomAccIncrement : public ExpandableToggleRow { + Q_OBJECT + +public: + CustomAccIncrement(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr); + +private: + QMap customLongValues = { + {"1", "1"}, + {"2", "5"}, // Default + {"3", "10"} + }; +}; + +class AccIncrementOptionControl : public OptionControlSP { + Q_OBJECT + +public: + AccIncrementOptionControl(const QString ¶m, const MinMaxValue &range, int per_value_change, const QMap *valMap = nullptr); + void refresh(); + +protected: + std::string param_name; + +private: + Params params; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc index 9d8a4d9c19..15ed10bf9a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc @@ -8,4 +8,72 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h" LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) { + main_layout = new QStackedLayout(this); + ListWidget *list = new ListWidget(this, false); + + cruisePanelScreen = new QWidget(this); + QVBoxLayout *vlayout = new QVBoxLayout(cruisePanelScreen); + vlayout->setContentsMargins(0, 0, 0, 0); + + cruisePanelScroller = new ScrollViewSP(list, this); + vlayout->addWidget(cruisePanelScroller); + + customAccIncrement = new CustomAccIncrement("CustomAccIncrementsEnabled", tr("Custom ACC Speed Increments"), "", "", this); + list->addItem(customAccIncrement); + + QObject::connect(uiState(), &UIState::offroadTransition, this, &LongitudinalPanel::refresh); + + main_layout->addWidget(cruisePanelScreen); + main_layout->setCurrentWidget(cruisePanelScreen); + refresh(offroad); +} + +void LongitudinalPanel::showEvent(QShowEvent *event) { + main_layout->setCurrentWidget(cruisePanelScreen); + refresh(offroad); +} + +void LongitudinalPanel::refresh(bool _offroad) { + auto cp_bytes = params.get("CarParamsPersistent"); + if (!cp_bytes.empty()) { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + + has_longitudinal_control = hasLongitudinalControl(CP); + is_pcm_cruise = CP.getPcmCruise(); + } else { + has_longitudinal_control = false; + is_pcm_cruise = false; + } + + QString accEnabledDescription = tr("Enable custom Short & Long press increments for cruise speed increase/decrease."); + QString accNoLongDescription = tr("This feature can only be used with openpilot longitudinal control enabled."); + QString accPcmCruiseDisabledDescription = tr("This feature is not supported on this platform due to vehicle limitations."); + QString onroadOnlyDescription = tr("Start the vehicle to check vehicle compatibility."); + + if (offroad) { + customAccIncrement->setDescription(onroadOnlyDescription); + customAccIncrement->showDescription(); + } else { + if (has_longitudinal_control) { + if (is_pcm_cruise) { + customAccIncrement->setDescription(accPcmCruiseDisabledDescription); + customAccIncrement->showDescription(); + } else { + customAccIncrement->setDescription(accEnabledDescription); + } + } else { + params.remove("CustomAccIncrementsEnabled"); + customAccIncrement->toggleFlipped(false); + customAccIncrement->setDescription(accNoLongDescription); + customAccIncrement->showDescription(); + } + } + + // enable toggle when long is available and is not PCM cruise + customAccIncrement->setEnabled(has_longitudinal_control && !is_pcm_cruise && !offroad); + customAccIncrement->refresh(); + + offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h index cd68e3ec2f..58e94f333b 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h @@ -7,11 +7,26 @@ #pragma once +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal/custom_acc_increment.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" class LongitudinalPanel : public QWidget { Q_OBJECT public: explicit LongitudinalPanel(QWidget *parent = nullptr); + void showEvent(QShowEvent *event) override; + void refresh(bool _offroad); + +private: + Params params; + bool has_longitudinal_control = false; + bool is_pcm_cruise = false; + bool offroad = false; + + QStackedLayout *main_layout = nullptr; + ScrollViewSP *cruisePanelScroller = nullptr; + QWidget *cruisePanelScreen = nullptr; + CustomAccIncrement *customAccIncrement = nullptr; }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc new file mode 100644 index 0000000000..b8237c3627 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -0,0 +1,312 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include +#include +#include + +#include "common/model.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +static const QString progressStyleActive = "QProgressBar {" + " font-size: 40px;" + " font-weight: 200;" + " padding: 1px;" + " border: 3px solid black;" + " border-radius: 10px;" + "}" + "QProgressBar::chunk {" + " background-color: #1e79e8;" + " border-radius: 10px;" + "}"; + +static const QString progressStyleInactive = progressStyleActive + + "QProgressBar::chunk {" + " background-color: transparent;" + "}"; + +static const QString progressStyleDone = progressStyleActive + + "QProgressBar {" + " color: #33ab4c;" + "}" + "QProgressBar::chunk {" + " background-color: transparent;" + "}"; + +static const QString progressStyleError = progressStyleActive + + "QProgressBar {" + " color: red;" + "}" + "QProgressBar::chunk {" + " background-color: transparent;" + "}"; + +ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(50, 20, 50, 20); + + ListWidgetSP *list = new ListWidgetSP(this, false); + ScrollViewSP *scroller = new ScrollViewSP(list, this); + main_layout->addWidget(scroller); + + const auto current_model = GetActiveModelName(); + currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), "", this); + currentModelLblBtn->setValue(current_model); + + connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &ModelsPanel::handleCurrentModelLblBtnClicked); + connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { + is_onroad = !offroad; + updateLabels(); + }); + connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels); + list->addItem(currentModelLblBtn); + + // Create progress bars for downloads + supercomboProgressBar = createProgressBar(this); + QString supercomboType = tr("Driving Model"); + supercomboFrame = createModelDetailFrame(this, supercomboType, supercomboProgressBar); + list->addItem(supercomboFrame); + + navigationProgressBar = createProgressBar(this); + QString navigationType = tr("Navigation Model"); + navigationFrame = createModelDetailFrame(this, navigationType, navigationProgressBar); + list->addItem(navigationFrame); + + visionProgressBar = createProgressBar(this); + QString visionType = tr("Vision Model"); + visionFrame = createModelDetailFrame(this, visionType, visionProgressBar); + list->addItem(visionFrame); + + policyProgressBar = createProgressBar(this); + QString policyType = tr("Policy Model"); + policyFrame = createModelDetailFrame(this, policyType, policyProgressBar); + list->addItem(policyFrame); + + list->addItem(horizontal_line()); + + // LiveDelay toggle + list->addItem(new ParamControlSP("LagdToggle", + tr("Live Learning Steer Delay"), + tr("Enable this for the car to learn and adapt its steering response time. " + "Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience."), + "../assets/offroad/icon_shell.png")); +} + +QProgressBar* ModelsPanel::createProgressBar(QWidget *parent) { + QProgressBar *progressBar = new QProgressBar(parent); + progressBar->setRange(0, 100); + progressBar->setValue(0); + progressBar->setTextVisible(true); + progressBar->setAlignment(Qt::AlignVCenter); + return progressBar; +} + +QFrame* ModelsPanel::createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar) { + QFrame *frame = new QFrame(parent); + QHBoxLayout *layout = new QHBoxLayout(frame); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(50); + layout->addWidget(new QLabel(typeName)); + layout->addWidget(progressBar); + frame->setVisible(false); + return frame; +} + +/** + * @brief Updates the UI with bundle download progress information + * Reads status from modelManagerSP cereal message and displays status for all models + */ +void ModelsPanel::handleBundleDownloadProgress() { + supercomboFrame->setVisible(false); + visionFrame->setVisible(false); + policyFrame->setVisible(false); + navigationFrame->setVisible(false); + + using DS = cereal::ModelManagerSP::DownloadStatus; + if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) { + return; + } + + const bool showSelectedBundle = model_manager.hasSelectedBundle() && (isDownloading() || model_manager.getSelectedBundle().getStatus() == DS::FAILED); + const auto &bundle = showSelectedBundle ? model_manager.getSelectedBundle() : model_manager.getActiveBundle(); + const auto &models = bundle.getModels(); + download_status = bundle.getStatus(); + const auto download_status_changed = prev_download_status != download_status; + QStringList status; + + // Get status for each model type in order + for (const auto &model: models) { + QString modelName = QString::fromStdString(bundle.getDisplayName()); + + QProgressBar *progressBar = nullptr; + QFrame *modelFrame = nullptr; + + switch (model.getType()) { + case cereal::ModelManagerSP::Model::Type::SUPERCOMBO: + progressBar = supercomboProgressBar; + modelFrame = supercomboFrame; + break; + case cereal::ModelManagerSP::Model::Type::NAVIGATION: + progressBar = navigationProgressBar; + modelFrame = navigationFrame; + break; + case cereal::ModelManagerSP::Model::Type::VISION: + progressBar = visionProgressBar; + modelFrame = visionFrame; + break; + case cereal::ModelManagerSP::Model::Type::POLICY: + progressBar = policyProgressBar; + modelFrame = policyFrame; + break; + } + + const auto &progress = model.getArtifact().getDownloadProgress(); + QString line; + + if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) { + progressBar->setStyleSheet(progressStyleActive); + progressBar->setValue(progress.getProgress()); + progressBar->setFormat(QString(" %1% - %2").arg(static_cast(progress.getProgress())).arg(modelName)); + device()->resetInteractiveTimeout(); + } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADED) { + progressBar->setStyleSheet(progressStyleDone); + progressBar->setFormat(tr(" %1 - %2").arg(modelName, download_status_changed ? tr("downloaded") : tr("ready"))); + } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::CACHED) { + progressBar->setStyleSheet(progressStyleDone); + progressBar->setFormat(tr(" %1 - %2").arg(modelName, download_status_changed ? tr("from cache") : tr("ready"))); + } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::FAILED) { + progressBar->setStyleSheet(progressStyleError); + progressBar->setFormat(tr(" download failed - %1").arg(modelName)); + } else { + progressBar->setStyleSheet(progressStyleInactive); + progressBar->setFormat(tr(" pending - %1").arg(modelName)); + } + // keep navigation hidden for now to avoid confusion + if (model.getType() != cereal::ModelManagerSP::Model::Type::NAVIGATION) { + modelFrame->setVisible(true); + } + } + prev_download_status = download_status; +} + +/** + * @brief Gets the name of the currently selected model bundle + * @return Display name of the selected bundle or default model name + */ +QString ModelsPanel::GetActiveModelName() { + if (model_manager.hasActiveBundle()) { + return QString::fromStdString(model_manager.getActiveBundle().getDisplayName()); + } + + return DEFAULT_MODEL; +} + +/** + * @brief Gets the short name of the currently selected model bundle + * @return Display short name of the selected bundle or default model name + */ +QString ModelsPanel::GetActiveModelInternalName() { + if (model_manager.hasActiveBundle()) { + return QString::fromStdString(model_manager.getActiveBundle().getInternalName()); + } + return DEFAULT_MODEL; +} + +void ModelsPanel::updateModelManagerState() { + const SubMaster &sm = *(uiStateSP()->sm); + model_manager = sm["modelManagerSP"].getModelManagerSP(); +} + +/** + * @brief Handles the model bundle selection button click + * Displays available bundles, allows selection, and initiates download + */ +void ModelsPanel::handleCurrentModelLblBtnClicked() { + currentModelLblBtn->setEnabled(false); + currentModelLblBtn->setValue(tr("Fetching models...")); + + // Create mapping of bundle indices to display names + QMap index_to_bundle; + const auto bundles = model_manager.getAvailableBundles(); + for (const auto &bundle: bundles) { + index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundle.getDisplayName())); + } + + // Sort bundles by index in descending order + QStringList bundleNames; + // Add "Default" as the first option + bundleNames.append(DEFAULT_MODEL); + + auto indices = index_to_bundle.keys(); + std::sort(indices.begin(), indices.end(), std::greater()); + for (const auto &index: indices) { + bundleNames.append(index_to_bundle[index]); + } + + currentModelLblBtn->setValue(GetActiveModelInternalName()); + + const QString selectedBundleName = MultiOptionDialog::getSelection( + tr("Select a Model"), bundleNames, GetActiveModelName(), this); + + if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) { + return; + } + + // Handle "Stock" selection differently + if (selectedBundleName == DEFAULT_MODEL) { + params.remove("ModelManager_ActiveBundle"); + currentModelLblBtn->setValue(tr("Default")); + showResetParamsDialog(); + } else { + // Find selected bundle and initiate download + for (const auto &bundle: bundles) { + if (QString::fromStdString(bundle.getDisplayName()) == selectedBundleName) { + params.put("ModelManager_DownloadIndex", std::to_string(bundle.getIndex())); + if (bundle.getGeneration() != model_manager.getActiveBundle().getGeneration()) { + showResetParamsDialog(); + } + break; + } + } + } + + updateLabels(); +} + +/** + * @brief Updates the UI elements based on current state + */ +void ModelsPanel::updateLabels() { + if (!isVisible()) { + return; + } + + updateModelManagerState(); + handleBundleDownloadProgress(); + currentModelLblBtn->setEnabled(!is_onroad && !isDownloading()); + currentModelLblBtn->setValue(GetActiveModelInternalName()); +} + +/** + * @brief Shows dialog prompting user to reset calibration after model download + */ +void ModelsPanel::showResetParamsDialog() { + const auto confirmMsg = QString("%1

%2

%3") + .arg(tr("Model download has started in the background.")) + .arg(tr("We STRONGLY suggest you to reset calibration.")) + .arg(tr("Would you like to do that now?")); + const auto button_text = tr("Reset Calibration"); + + QString content("

" + tr("Driving Model Selector") + "


" + "

" + confirmMsg + "

"); + + if (showConfirmationDialog(content, button_text, false)) { + params.remove("CalibrationParams"); + params.remove("LiveTorqueParameters"); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h new file mode 100644 index 0000000000..8b8cf69759 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" + +class ModelsPanel : public QWidget { + Q_OBJECT + +public: + explicit ModelsPanel(QWidget *parent = nullptr); + +private: + QString GetActiveModelName(); + QString GetActiveModelInternalName(); + void updateModelManagerState(); + + bool isDownloading() const { + if (!model_manager.hasSelectedBundle()) { + return false; + } + + const auto &selected_bundle = model_manager.getSelectedBundle(); + return selected_bundle.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING; + } + + // UI update related methods + void updateLabels(); + void handleCurrentModelLblBtnClicked(); + void handleBundleDownloadProgress(); + void showResetParamsDialog(); + QProgressBar* createProgressBar(QWidget *parent); + QFrame* createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar); + cereal::ModelManagerSP::Reader model_manager; + cereal::ModelManagerSP::DownloadStatus download_status{}; + cereal::ModelManagerSP::DownloadStatus prev_download_status{}; + + bool canContinueOnMeteredDialog() { + if (!is_metered) return true; + return showConfirmationDialog(QString(), QString(), is_metered); + } + + inline bool showConfirmationDialog(const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) { + return showConfirmationDialog(this, message, confirmButtonText, show_metered_warning); + } + + static inline bool showConfirmationDialog(QWidget *parent, const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) { + const QString warning_message = show_metered_warning ? tr("Warning: You are on a metered connection!") : QString(); + const QString final_message = QString("%1%2").arg(!message.isEmpty() ? message + "\n" : QString(), warning_message); + const QString final_buttonText = !confirmButtonText.isEmpty() ? confirmButtonText : QString(tr("Continue") + " %1").arg(show_metered_warning ? tr("on Metered") : ""); + + return ConfirmationDialog(final_message, final_buttonText, tr("Cancel"), true, parent).exec(); + } + + bool is_metered{}; + bool is_wifi{}; + bool is_onroad = false; + + ButtonControlSP *currentModelLblBtn; + QProgressBar *supercomboProgressBar; + QFrame *supercomboFrame; + QProgressBar *navigationProgressBar; + QFrame *navigationFrame; + QProgressBar *visionProgressBar; + QFrame *visionFrame; + QProgressBar *policyProgressBar; + QFrame *policyFrame; + Params params; + +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h new file mode 100644 index 0000000000..32d169ad15 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include // for std::sort +#include +#include +#include + +#include +#include + +#include "selfdrive/ui/sunnypilot/qt/common/json_fetcher.h" + +static const std::tuple defaultLocation = std::make_tuple("== None ==", ""); + +// New class LocationsFetcher that handles web requests and JSON parsing +class LocationsFetcher { +public: + inline std::vector > + getLocationsFromURL(const QUrl &url, const std::tuple &customLocation = defaultLocation) const { + // Initialize an empty vector to hold the locations + std::vector > locations; + + JsonFetcher fetcher; + QJsonObject json = fetcher.getJsonFromURL(url.toString()); + + for (auto it = json.begin(); it != json.end(); ++it) { + QString code = it.key(); + QJsonObject obj = it.value().toObject(); + QString fullName = obj["full_name"].toString(); + + locations.push_back(std::make_tuple(fullName, code, QString(), QString())); + } + // Sort locations by full name + std::sort(locations.begin(), locations.end(), [](const auto &lhs, const auto &rhs) { + return std::get<0>(lhs) < std::get<0>(rhs); // Compare full names + }); + // Optionally, you can now add defaultName entry at the beginning + locations.insert(locations.begin(), std::tuple_cat(customLocation, std::make_tuple("", ""))); + return locations; + } + + inline std::vector > + getLocationsFromURL(const QString &url, const std::tuple &customLocation = defaultLocation) const { + return getLocationsFromURL(QUrl(url), customLocation); + } + + inline std::vector > + getOsmLocations(const std::tuple &customLocation = defaultLocation) const { + return getLocationsFromURL( "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/nation_bounding_boxes.json", customLocation); + } + + inline std::vector > + getUsStatesLocations(const std::tuple &customLocation = defaultLocation) const { + return getLocationsFromURL( "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/us_states_bounding_boxes.json", customLocation); + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.cc new file mode 100644 index 0000000000..b67e41e09d --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.cc @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h" + +#include + +ModelsFetcher::ModelsFetcher(QObject *parent) : QObject(parent) { + manager = new QNetworkAccessManager(this); +} + +QByteArray ModelsFetcher::verifyFileHash(const QString &filePath, const QString &expectedHash, bool &hashMatches) { + hashMatches = false; // Default to false + QByteArray fileData; + + if (expectedHash.isEmpty()) { + // If no hash is provided, assume verification isn't required but return the file data + hashMatches = true; + } else { + QFile file(filePath); + if (file.open(QIODevice::ReadOnly)) { + QCryptographicHash hash(QCryptographicHash::Sha256); // Or your chosen algorithm + fileData = file.readAll(); // Read the file data once + hash.addData(fileData); + file.close(); + + QString currentHash = QString(hash.result().toHex()); + hashMatches = (currentHash == expectedHash); + } + } + + // Return the file data if hash matches or no hash was provided; empty otherwise + return hashMatches ? fileData : QByteArray(); +} + + +void ModelsFetcher::download(const DownloadInfo &downloadInfo, const QString &filename, const QString &destinationPath) { + QString fullPath = destinationPath + "/" + filename; + QFileInfo fileInfo(fullPath); + bool hashMatches = false; + QByteArray data = verifyFileHash(fullPath, downloadInfo.sha256, hashMatches); + + if (fileInfo.exists() && hashMatches) { + // Hash matches or no hash provided, and we have the file data + LOGD("File already downloaded and verified: %s", filename.toStdString().c_str()); + emit downloadProgress(100); + emit downloadComplete(data, true); // Use the data returned from verifyFileHash + return; // Exit early + } + + // Proceed with download if file does not exist or hash verification failed + QNetworkRequest request(downloadInfo.url); + QNetworkReply *reply = manager->get(request); + connect(reply, &QNetworkReply::downloadProgress, this, &ModelsFetcher::onDownloadProgress); + connect(reply, &QNetworkReply::finished, this, [this, reply, destinationPath, filename, downloadInfo]() { + onFinished(reply, destinationPath, filename, downloadInfo.sha256); + }); +} + +QString extractFileName(const QString &contentDisposition) { + const QString filenameTag = "filename="; + const int idx = contentDisposition.indexOf(filenameTag); + if (idx < 0) { + return QString(); + } + + QString filename = contentDisposition.mid(idx + filenameTag.length()); + if (filename.startsWith("\"") && filename.endsWith("\"")) { + return filename.mid(1, filename.size() - 2); + } + + return filename; +} + +void ModelsFetcher::onFinished(QNetworkReply *reply, const QString &destinationPath, const QString &filename, const QString &expectedHash) { + // Handle download error + if (reply->error()) { + return; // Possibly emit a signal or log an error as per your error handling policy + } + + const QByteArray data = reply->readAll(); + + QString finalFilename = filename; + if (finalFilename.isEmpty()) { + finalFilename = extractFileName(reply->header(QNetworkRequest::ContentDispositionHeader).toString()); + } + + QString finalPath = QDir(destinationPath).filePath(finalFilename); + + // Save the downloaded file + QFile file(finalPath); + //ensure if the path exists and if not create it + if (!QDir().mkpath(destinationPath)) { + LOGE("Unable to create directory: %s", destinationPath.toStdString().c_str()); + emit downloadFailed(filename); + return; // Stop further processing + } + + //Retry the file open and write 3 times with a little delay between each retry + for (int i = 0; i < 3; i++) { + if (file.isOpen()) break; + + file.open(QIODevice::WriteOnly); + if (!file.isOpen()) QThread::msleep(100); + } + + // If the file is still not open, log an error and emit a failure signal + if (!file.isOpen()) { + LOGE("Unable to open file for writing: %s", finalPath.toStdString().c_str()); + emit downloadFailed(filename); + return; // Stop further processing + } + + file.write(data); + file.close(); + + bool hashMatches = false; + verifyFileHash(finalPath, expectedHash, hashMatches); + + // Verify the file hash if expectedHash is provided + if (!expectedHash.isEmpty() && !hashMatches) { + LOGE("The downloaded file didn't pass the hash validation!: %s", filename.toStdString().c_str()); + // Hash verification failed, handle accordingly + // This could involve deleting the file, logging an error, or emitting a failure signal + QFile::remove(finalPath); // Example action: Remove the invalid file + emit downloadFailed(filename); + return; // Stop further processing + } + + emit downloadComplete(data, false); // Emit your success signal +} + +void ModelsFetcher::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { + const double progress = (bytesReceived * 100.0) / bytesTotal; + emit downloadProgress(progress); +} + +std::vector ModelsFetcher::getModelsFromURL(const QUrl &url) { + std::vector models; + JsonFetcher fetcher; + QJsonObject json = fetcher.getJsonFromURL(url.toString()); + for (auto it = json.begin(); it != json.end(); ++it) { + models.push_back(Model(it.value().toObject())); + } + return models; +} + +std::vector ModelsFetcher::getModelsFromURL(const QString &url) { + return getModelsFromURL(QUrl(url)); +} + +std::vector ModelsFetcher::getModelsFromURL() { + return getModelsFromURL("https://docs.sunnypilot.ai/models_v5.json"); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h new file mode 100644 index 0000000000..73890e9d5a --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm/models_fetcher.h @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include // for std::sort +#include +#include +#include + +#include +#include +#include + +#include "common/swaglog.h" +#include "common/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/common/json_fetcher.h" +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#else +#include "selfdrive/ui/qt/widgets/controls.h" +#endif +#include "system/hardware/hw.h" + +static const QString MODELS_PATH = Hardware::PC() ? QDir::homePath() + "/.comma/media/0/models/" : "/data/media/0/models/"; + +struct DownloadInfo { + QString url; + QString sha256; +}; + +// New class ModelsFetcher with a new function that handles web requests and JSON parsing for the new JSON structure +class Model { +public: + explicit Model(const QJsonObject &json) { + displayName = json["display_name"].toString(); + fullName = json["full_name"].toString(); + fileName = json["file_name"].toString(); + + // Parse downloadUri as an object + QJsonObject downloadUriObj = json["download_uri"].toObject(); + downloadUri.url = downloadUriObj["url"].toString(); + downloadUri.sha256 = downloadUriObj["sha256"].toString(); + + fullNameNav = json["full_name_nav"].toString(); + fileNameNav = json["file_name_nav"].toString(); + + // Parse downloadUriNav as an object + QJsonObject downloadUriNavObj = json["download_uri_nav"].toObject(); + downloadUriNav.url = downloadUriNavObj["url"].toString(); + downloadUriNav.sha256 = downloadUriNavObj["sha256"].toString(); + + fullNameMetadata = json["full_name_metadata"].toString(); + fileNameMetadata = json["file_name_metadata"].toString(); + + // Parse downloadUriMetadata as an object + QJsonObject downloadUriMetadataObj = json["download_uri_metadata"].toObject(); + downloadUriMetadata.url = downloadUriMetadataObj["url"].toString(); + downloadUriMetadata.sha256 = downloadUriMetadataObj["sha256"].toString(); + + index = json["index"].toString(); + environment = json["environment"].toString(); + generation = json["generation"].toString(); + } + + // Method to convert model back to QJsonObject, if needed + QJsonObject toJson() const { + QJsonObject json; + json["display_name"] = displayName; + json["full_name"] = fullName; + json["file_name"] = fileName; + + QJsonObject uriObj; + uriObj["url"] = downloadUri.url; + uriObj["sha256"] = downloadUri.sha256; + json["download_uri"] = uriObj; + + QJsonObject uriNavObj; + uriNavObj["url"] = downloadUriNav.url; + uriNavObj["sha256"] = downloadUriNav.sha256; + json["download_uri_nav"] = uriNavObj; + + QJsonObject uriMetadataObj; + uriMetadataObj["url"] = downloadUriMetadata.url; + uriMetadataObj["sha256"] = downloadUriMetadata.sha256; + json["download_uri_metadata"] = uriMetadataObj; + + json["full_name_nav"] = fullNameNav; + json["file_name_nav"] = fileNameNav; + json["full_name_metadata"] = fullNameMetadata; + json["file_name_metadata"] = fileNameMetadata; + json["index"] = index; + json["environment"] = environment; + json["generation"] = generation; + return json; + } + + QString displayName; + QString fullName; + QString fileName; + DownloadInfo downloadUri; + DownloadInfo downloadUriNav; + DownloadInfo downloadUriMetadata; + + QString fullNameNav; + QString fileNameNav; + QString fullNameMetadata; + QString fileNameMetadata; + QString index; + QString environment; + QString generation; +}; + +class ModelsFetcher : public QObject { + Q_OBJECT + +public: + explicit ModelsFetcher(QObject *parent = nullptr); + void download(const DownloadInfo &url, const QString &filename = "", const QString &destinationPath = MODELS_PATH); + static std::vector getModelsFromURL(const QUrl &url); + static std::vector getModelsFromURL(const QString &url); + static std::vector getModelsFromURL(); + +signals: + void downloadProgress(double percentage); + void downloadComplete(const QByteArray &data, bool fromCache = false); + void downloadFailed(const QString &filename); + +private: + // static bool verifyFileHash(const QString& filePath, const QString& expectedHash); + static QByteArray verifyFileHash(const QString &filePath, const QString &expectedHash, bool &hashMatches); + void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void onFinished(QNetworkReply *reply, const QString &destinationPath, const QString &filename, + const QString &expectedHash); + + QNetworkAccessManager *manager; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.cc new file mode 100644 index 0000000000..332fd71a18 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.cc @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h" + +#include +#include +#include + +#include "common/swaglog.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +OsmPanel::OsmPanel(QWidget *parent) : QFrame(parent) { + main_layout = new QStackedLayout(this); + + const auto list = new ListWidgetSP(this, false); + list->addItem(mapdVersion = new LabelControlSP(tr("Mapd Version"), "Loading...")); + list->addItem(setupOsmDeleteMapsButton(parent)); + list->addItem(offlineMapsETA = new LabelControlSP(tr("Offline Maps ETA"), "")); + list->addItem(offlineMapsElapsed = new LabelControlSP(tr("Time Elapsed"), "")); + list->addItem(setupOsmUpdateButton(parent)); + list->addItem(setupOsmDownloadButton(parent)); + list->addItem(setupUsStatesButton(parent)); + + connect(uiStateSP(), &UIStateSP::offroadTransition, [=](bool offroad) { + updateLabels(); + }); + + timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, QOverload<>::of(&OsmPanel::updateLabels)); + timer->start(FAST_REFRESH_INTERVAL); // Time specified in milliseconds. + updateLabels(); + + osmScreen = new QWidget(this); + auto *vlayout = new QVBoxLayout(osmScreen); + vlayout->setContentsMargins(50, 20, 50, 20); + vlayout->addWidget(new ScrollViewSP(list, this), 1); + main_layout->addWidget(osmScreen); +} + +ButtonControlSP *OsmPanel::setupOsmDeleteMapsButton(QWidget *parent) { + osmDeleteMapsBtn = new ButtonControlSP(tr("Downloaded Maps"), tr("DELETE")); // Updated on updateLabels() + connect(osmDeleteMapsBtn, &ButtonControlSP::clicked, [=]() { + if (showConfirmationDialog(parent, tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all the maps?"), tr("Yes, delete all the maps."))) { + QtConcurrent::run([=]() { + QDir dir(MAP_PATH); + osmDeleteMapsBtn->setEnabled(false); + osmDeleteMapsBtn->setText("⌛"); + dir.removeRecursively(); + updateMapSize(); + osmDeleteMapsBtn->setEnabled(true); + osmDeleteMapsBtn->setText(tr("DELETE")); + }); + updateLabels(); + } + }); + return osmDeleteMapsBtn; +} + +ButtonControlSP *OsmPanel::setupOsmUpdateButton(QWidget *parent) { + osmUpdateBtn = new ButtonControlSP(tr("Database Update"), tr("CHECK")); // Updated on updateLabels() + connect(osmUpdateBtn, &ButtonControlSP::clicked, [=]() { + if (osm_download_in_progress && !download_failed_state) { + updateLabels(); + } else if (showConfirmationDialog(parent)) { + osm_download_in_progress = true; + params.putBool("OsmDbUpdatesCheck", true); + updateLabels(); + } + }); + return osmUpdateBtn; +} + +ButtonControlSP *OsmPanel::setupOsmDownloadButton(QWidget *parent) { + osmDownloadBtn = new ButtonControlSP(tr("Country"), tr("SELECT")); + connect(osmDownloadBtn, &ButtonControlSP::clicked, [=]() { + osmDownloadBtn->setEnabled(false); + osmDownloadBtn->setValue(tr("Fetching Country list...")); + const std::vector > locations = getOsmLocations(); + osmDownloadBtn->setEnabled(true); + osmDownloadBtn->setValue(""); + const QString initTitle = QString::fromStdString(params.get("OsmLocationTitle")); + const QString currentTitle = ((initTitle == "== None ==") || (initTitle.length() == 0)) ? "== None ==" : initTitle; + + QStringList locationTitles; + for (auto &loc: locations) { + locationTitles.push_back(std::get<0>(loc)); + } + + const QString selection = MultiOptionDialog::getSelection(tr("Country"), locationTitles, currentTitle, this); + if (!selection.isEmpty()) { + params.put("OsmLocal", "1"); + params.put("OsmLocationTitle", selection.toStdString()); + for (auto &loc: locations) { + if (std::get<0>(loc) == selection) { + params.put("OsmLocationName", std::get<1>(loc).toStdString()); + break; + } + } + if (params.get("OsmLocationName") == "US") { + usStatesBtn->click(); + return; + } else if (selection != "== None ==") { + if (showConfirmationDialog(parent)) { + osm_download_in_progress = true; + params.putBool("OsmDbUpdatesCheck", true); + updateLabels(); + } + } + } + updateLabels(); + }); + return osmDownloadBtn; +} + +ButtonControlSP *OsmPanel::setupUsStatesButton(QWidget *parent) { + usStatesBtn = new ButtonControlSP(tr("State"), tr("SELECT")); + connect(usStatesBtn, &ButtonControlSP::clicked, [=]() { + const std::tuple allStatesOption = std::make_tuple("All States (~4.8 GB)", "All"); + usStatesBtn->setEnabled(false); + usStatesBtn->setValue(tr("Fetching State list...")); + const std::vector > locations = + getUsStatesLocations(allStatesOption); + usStatesBtn->setEnabled(true); + usStatesBtn->setValue(""); + const QString initTitle = QString::fromStdString(params.get("OsmStateTitle")); + const QString currentTitle = ((initTitle == std::get<0>(allStatesOption)) || (initTitle.length() == 0)) ? tr("All") : initTitle; + + QStringList locationTitles; + for (auto &loc: locations) { + locationTitles.push_back(std::get<0>(loc)); + } + + const QString selection = MultiOptionDialog::getSelection(tr("State"), locationTitles, currentTitle, this); + if (!selection.isEmpty()) { + params.put("OsmStateTitle", selection.toStdString()); + for (auto &loc: locations) { + if (std::get<0>(loc) == selection) { + params.put("OsmStateName", std::get<1>(loc).toStdString()); + break; + } + } + usStatesBtn->setValue(selection); + if (showConfirmationDialog(parent)) { + osm_download_in_progress = true; + params.putBool("OsmDbUpdatesCheck", true); + updateLabels(); + } + } + updateLabels(); + }); + usStatesBtn->setVisible(false); // initially hidden + return usStatesBtn; +} + +void OsmPanel::showEvent(QShowEvent *event) { + updateLabels(); // For snappier feeling + if (!timer->isActive()) { + timer->start(FAST_REFRESH_INTERVAL); + } +} + +void OsmPanel::hideEvent(QHideEvent *event) { + if (timer->isActive()) { + timer->stop(); + } +} + + +void OsmPanel::updateLabels() { + if (!isVisible()) { + return; + } + mapd_version = params.get("MapdVersion"); + mapdVersion->setText(mapd_version.c_str()); + + updateMapSize(); + osm_download_locations = mem_params.get("OSMDownloadLocations"); + osm_download_in_progress = !osm_download_locations.empty(); + + timer->setInterval(osm_download_in_progress ? FAST_REFRESH_INTERVAL : SLOW_REFRESH_INTERVAL); + LOGT("Timer Interval %d", timer->interval()); + + const std::string osmLastDownloadTimeStr = params.get("OsmDownloadedDate"); + if (!lastDownloadedTimePoint.has_value() && !osmLastDownloadTimeStr.empty()) { + const double osmLastDownloadTime = std::stod(osmLastDownloadTimeStr); + lastDownloadedTimePoint = std::chrono::system_clock::from_time_t(static_cast(osmLastDownloadTime)); + } + + osmDownloadBtn->setEnabled(!osm_download_in_progress); + usStatesBtn->setEnabled(!osm_download_in_progress); + + updateDownloadProgress(); + + const QString locationName = QString::fromStdString(params.get("OsmLocationName")); + const bool isUs = !locationName.isEmpty() && locationName == "US"; + usStatesBtn->setVisible(isUs); + + if (!locationName.isEmpty()) { + if (!isUs) { + params.remove("OsmStateName"); + params.remove("OsmStateTitle"); + } + osmUpdateBtn->setVisible(true); + } else { + params.remove("OsmLocal"); + params.remove("OsmLocationName"); + params.remove("OsmLocationTitle"); + params.remove("OsmStateName"); + params.remove("OsmStateTitle"); + osmUpdateBtn->setVisible(false); + usStatesBtn->setVisible(false); + } + + osmDownloadBtn->setValue(QString::fromStdString(params.get("OsmLocationTitle"))); + usStatesBtn->setValue(QString::fromStdString(params.get("OsmStateTitle"))); + update(); +} + +void OsmPanel::updateDownloadProgress() { + const auto pending_update_check = params.getBool("OsmDbUpdatesCheck"); + const QJsonObject osmDownloadProgress = QJsonDocument::fromJson(params.get("OSMDownloadProgress").c_str()).object(); + if (osm_download_in_progress && lastDownloadedTimePoint.has_value()) { + offlineMapsETA->setVisible(true); + offlineMapsElapsed->setVisible(true); + offlineMapsETA->setText(calculateETA(osmDownloadProgress, lastDownloadedTimePoint.value())); + offlineMapsElapsed->setText(calculateElapsedTime(osmDownloadProgress, lastDownloadedTimePoint.value())); + } else { + offlineMapsETA->setVisible(false); + offlineMapsElapsed->setVisible(false); + } + + const int total_files = extractIntFromJson(osmDownloadProgress, "total_files"); + const int downloaded_files = extractIntFromJson(osmDownloadProgress, "downloaded_files"); + download_failed_state = total_files && osm_download_in_progress && !lastDownloadedTimePoint.has_value() && downloaded_files < total_files; + + QString updateButtonText = processUpdateStatus(pending_update_check, total_files, downloaded_files, osmDownloadProgress, download_failed_state); + + osmUpdateBtn->setValue(updateButtonText); + osmUpdateBtn->setText(osm_download_in_progress && !download_failed_state ? tr("REFRESH") : tr("UPDATE")); + osmDeleteMapsBtn->setValue(formatSize(mapsDirSize)); +} + +int OsmPanel::extractIntFromJson(const QJsonObject &json, const QString &key) { + return (json.contains(key)) ? json[key].toInt() : 0; +} + +QString OsmPanel::processUpdateStatus(bool pending_update, int total_files, int downloaded_files, const QJsonObject &json, bool failed_state) { + if (pending_update && !osm_download_in_progress && !total_files) { + lastDownloadedTimePoint.reset(); + return tr("Download starting..."); + } else if (failed_state) { + return tr("Error: Invalid download. Retry."); + } else if (osm_download_in_progress && total_files > downloaded_files) { + return formatDownloadStatus(json); + } else if (osm_download_in_progress && downloaded_files >= total_files) { + osm_download_in_progress = false; + lastDownloadedTimePoint.reset(); + return tr("Download complete!"); + } + + if (lastDownloadedTimePoint.has_value()) { + QDateTime dateTime = QDateTime::fromTime_t(std::chrono::system_clock::to_time_t(lastDownloadedTimePoint.value())); //fromMSecsSinceEpoch(duration); + dateTime = dateTime.toLocalTime(); + return QString("%1").arg(dateTime.toString("yyyy-MM-dd HH:mm:ss")); + } + + return ""; +} + +void OsmPanel::updateMapSize() { + if (mapSizeFuture.has_value() && mapSizeFuture.value().isFinished()) { + mapsDirSize = mapSizeFuture.value().result(); + } + + if (!mapSizeFuture.has_value() || !mapSizeFuture.value().isRunning()) { + mapSizeFuture = QtConcurrent::run(getDirSize, MAP_PATH); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h new file mode 100644 index 0000000000..171129a11c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h @@ -0,0 +1,232 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "selfdrive/ui/qt/network/wifi_manager.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm/locations_fetcher.h" +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/ui.h" +#include "system/hardware/hw.h" + +constexpr int FAST_REFRESH_INTERVAL = 1000; // ms +constexpr int SLOW_REFRESH_INTERVAL = 5000; // ms + +static const QString MAP_PATH = Hardware::PC() ? QDir::homePath() + "/.comma/media/0/osm/offline/" : "/data/media/0/osm/offline/"; + +class OsmPanel : public QFrame { + Q_OBJECT + +public: + explicit OsmPanel(QWidget *parent = nullptr); + +private: + QStackedLayout *main_layout = nullptr; + QWidget *osmScreen = nullptr; + Params params; + Params mem_params{Hardware::PC() ? "" : "/dev/shm/params"}; + std::map toggles; + std::optional > mapSizeFuture; + const SubMaster &sm = *uiStateSP()->sm; + + + bool is_onroad = false; + std::string mapd_version; + + bool isWifi() const { return sm["deviceState"].getDeviceState().getNetworkType() == cereal::DeviceState::NetworkType::WIFI; } + bool isMetered() const { return sm["deviceState"].getDeviceState().getNetworkMetered(); } + bool osm_download_in_progress = false; + bool download_failed_state = false; + quint64 mapsDirSize = 0; + + QLabel *osmUpdateLbl; + ButtonControlSP *osmDownloadBtn; + ButtonControlSP *osmUpdateBtn; + ButtonControlSP *usStatesBtn; + ButtonControlSP *osmDeleteMapsBtn; + + ButtonControlSP *setupOsmDeleteMapsButton(QWidget *parent);; + ButtonControlSP *setupOsmUpdateButton(QWidget *parent); + ButtonControlSP *setupOsmDownloadButton(QWidget *parent); + ButtonControlSP *setupUsStatesButton(QWidget *parent); + + QTimer *timer; + std::string osm_download_locations; + // void updateButtonControlSP(ButtonControlSP *btnControl, QWidget *parent, const QString &initTitle, const QString &allStatesOption); + + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + void updateLabels(); + void updateDownloadProgress(); + static int extractIntFromJson(const QJsonObject &json, const QString &key); + QString processUpdateStatus(bool pending_update_check, int total_files, int downloaded_files, const QJsonObject &json, bool failed_state); + + ConfirmationDialog *confirmationDialog; + LabelControlSP *mapdVersion; + LabelControlSP *offlineMapsStatus; + LabelControlSP *offlineMapsETA; + LabelControlSP *offlineMapsElapsed; + std::optional lastDownloadedTimePoint; + LocationsFetcher locationsFetcher; + + void updateMapSize(); + + bool showConfirmationDialog(QWidget *parent, + const QString &message = QString(), + const QString &confirmButtonText = QString()) const { + const auto _is_metered = isMetered(); + const QString warning_message = _is_metered ? tr("\n\nWarning: You are on a metered connection!") : QString(); + QString final_message = message.isEmpty() ? tr("This will start the download process and it might take a while to complete.") : message; + final_message += warning_message; // Append the warning message if the connection is metered + + const QString final_buttonText = confirmButtonText.isEmpty() ? (_is_metered ? tr("Continue on Metered") : tr("Start Download")) : confirmButtonText; + + return ConfirmationDialog::confirm(final_message, final_buttonText, parent); + } + + // Refactored methods + std::vector > getOsmLocations(const std::tuple &customLocation = defaultLocation) const { + return locationsFetcher.getOsmLocations(customLocation); + } + + std::vector > getUsStatesLocations(const std::tuple &customLocation = defaultLocation) const { + return locationsFetcher.getUsStatesLocations(customLocation); + } + + static QString formatTime(const long timeInSeconds) { + const long minutes = timeInSeconds / 60; + const long seconds = timeInSeconds % 60; + + QString formattedTime; + if (minutes > 0) { + formattedTime = QString::number(minutes) + tr("m "); + } + formattedTime += QString::number(seconds) + tr("s"); + return formattedTime; + } + + static QString calculateElapsedTime(const QJsonObject &jsonData, const std::chrono::system_clock::time_point &startTime) { + using namespace std::chrono; + if (!jsonData.contains("total_files") || !jsonData.contains("downloaded_files")) + return tr("Calculating..."); + + const int totalFiles = jsonData["total_files"].toInt(); + const int downloadedFiles = jsonData["downloaded_files"].toInt(); + + if (downloadedFiles >= totalFiles || totalFiles <= 0) return tr("Downloaded"); + + const long elapsed = duration_cast(system_clock::now() - startTime).count(); + + if (elapsed == 0 || downloadedFiles == 0) return tr("Calculating..."); + + return formatTime(elapsed); + } + + static QString calculateETA(const QJsonObject &jsonData, const std::chrono::system_clock::time_point &startTime) { + using namespace std::chrono; + static steady_clock::time_point lastUpdateTime = steady_clock::now(); + static std::deque rateHistory; + + constexpr int minDataPoints = 3; + constexpr int historySize = 10; + + static QString lastETA = tr("Calculating ETA..."); + + if (duration_cast(steady_clock::now() - lastUpdateTime).count() < 1) { + return lastETA; + } + + if (!jsonData.contains("total_files") || !jsonData.contains("downloaded_files")) + return lastETA; + + const int totalFiles = jsonData["total_files"].toInt(); + const int downloadedFiles = jsonData["downloaded_files"].toInt(); + + if (totalFiles <= 0 || downloadedFiles >= totalFiles) { + return totalFiles <= 0 ? tr("Ready") : tr("Downloaded"); + } + + const long elapsed = duration_cast(system_clock::now() - startTime).count(); + if (elapsed == 0 || downloadedFiles == 0) return lastETA; + + const double rate = downloadedFiles / static_cast(elapsed); + if (rateHistory.size() >= historySize) rateHistory.pop_front(); + rateHistory.push_back(rate); + + if (rateHistory.size() < minDataPoints) return lastETA; + + double weightedSum = 0; + for (int i = 0, weight = 1; i < rateHistory.size(); ++i, ++weight) { + weightedSum += rateHistory[i] * weight; + } + const double avgRate = 2 * weightedSum / (rateHistory.size() * (rateHistory.size() + 1)); + + const long remainingTime = static_cast((totalFiles - downloadedFiles) / avgRate); + if (remainingTime <= 0) return lastETA; + + lastETA = tr("Time remaining: ") + formatTime(remainingTime); + lastUpdateTime = steady_clock::now(); + return lastETA; + } + + static QString formatDownloadStatus(const QJsonObject &json) { + if (!json.contains("total_files") || !json.contains("downloaded_files")) + return ""; + + const int total_files = json["total_files"].toInt(); + const int downloaded_files = json["downloaded_files"].toInt(); + + if (total_files <= 0) return tr("Ready"); + if (downloaded_files >= total_files) return tr("Downloaded"); + + const int percentage = static_cast(100.0 * downloaded_files / total_files); + return QString::asprintf("%d/%d (%d%%)", downloaded_files, total_files, percentage); + } + + QString formatSize(quint64 size) const { + if (size == 0 && (!mapSizeFuture.has_value() || mapSizeFuture.value().isRunning())) { + return tr("Calculating..."); + } + + constexpr qint64 kb = 1024; + constexpr qint64 mb = 1024 * kb; + constexpr qint64 gb = 1024 * mb; + + if (size < gb) { + const double sizeMB = size / static_cast(mb); + return QString::number(sizeMB, 'f', 2) + " MB"; + } else { + const double sizeGB = size / static_cast(gb); + return QString::number(sizeGB, 'f', 2) + " GB"; + } + } + + static quint64 getDirSize(QString dirPath) { + quint64 size = 0; + const QString actualDirPath = dirPath.startsWith("~") ? dirPath.replace(0, 1, QDir::homePath()) : dirPath; + QDirIterator it(actualDirPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + if (it.fileInfo().isFile()) { + size += it.fileInfo().size(); + } + } + return size; + } +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index 48a395dad7..05ea5ff899 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -13,12 +13,15 @@ #include "selfdrive/ui/sunnypilot/qt/network/networking.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/osm_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/trips_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle_panel.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h" TogglesPanelSP::TogglesPanelSP(SettingsWindowSP *parent) : TogglesPanel(parent) { QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &TogglesPanelSP::updateState); @@ -69,6 +72,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { TogglesPanelSP *toggles = new TogglesPanelSP(this); QObject::connect(this, &SettingsWindowSP::expandToggleDescription, toggles, &TogglesPanel::expandToggleDescription); + QObject::connect(this, &SettingsWindowSP::scrollToToggle, toggles, &TogglesPanel::scrollToToggle); auto networking = new NetworkingSP(this); QObject::connect(uiState()->prime_state, &PrimeState::changed, networking, &NetworkingSP::setPrimeType); @@ -79,8 +83,11 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("sunnylink"), new SunnylinkPanel(this), "../assets/icons/wifi_strength_full.svg"), PanelInfo(" " + tr("Toggles"), toggles, "../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), PanelInfo(" " + tr("Software"), new SoftwarePanelSP(this), "../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), + PanelInfo(" " + tr("Models"), new ModelsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_models.png"), PanelInfo(" " + tr("Steering"), new LateralPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"), PanelInfo(" " + tr("Cruise"), new LongitudinalPanel(this), "../assets/icons/speed_limit.png"), + PanelInfo(" " + tr("Visuals"), new VisualsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"), + PanelInfo(" " + tr("OSM"), new OsmPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_map.png"), PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"), diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc index 20f3fdb598..9430d02ba8 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -7,201 +7,45 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" -#include -#include - -#include "common/model.h" - -/** - * @brief Constructs the software panel with model bundle selection functionality - * @param parent Parent widget - */ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) { - const auto current_model = GetActiveModelName(); - currentModelLblBtn = new ButtonControlSP(tr("Current Model"), tr("SELECT"), current_model); - currentModelLblBtn->setValue(current_model); - - connect(currentModelLblBtn, &ButtonControlSP::clicked, this, &SoftwarePanelSP::handleCurrentModelLblBtnClicked); - QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &SoftwarePanelSP::updateLabels); - AddWidgetAt(0, currentModelLblBtn); -} - -/** - * @brief Updates the UI with bundle download progress information - * Reads status from modelManagerSP cereal message and displays status for all models - */ -void SoftwarePanelSP::handleBundleDownloadProgress() { - using DS = cereal::ModelManagerSP::DownloadStatus; - if (!model_manager.hasSelectedBundle() && !model_manager.hasActiveBundle()) { - currentModelLblBtn->setDescription(tr("No custom model selected!")); - return; - } - - const bool showSelectedBundle = model_manager.hasSelectedBundle() && (isDownloading() || model_manager.getSelectedBundle().getStatus() == DS::FAILED); - const auto &bundle = showSelectedBundle ? model_manager.getSelectedBundle() : model_manager.getActiveBundle(); - const auto &models = bundle.getModels(); - download_status = bundle.getStatus(); - const auto download_status_changed = prev_download_status != download_status; - QStringList status; - - // Get status for each model type in order - for (const auto &model: models) { - QString typeName; - QString modelName = QString::fromStdString(bundle.getDisplayName()); - - switch (model.getType()) { - case cereal::ModelManagerSP::Model::Type::SUPERCOMBO: - typeName = tr("Driving"); - break; - case cereal::ModelManagerSP::Model::Type::NAVIGATION: - typeName = tr("Navigation"); - break; - case cereal::ModelManagerSP::Model::Type::VISION: - typeName = tr("Vision"); - break; - case cereal::ModelManagerSP::Model::Type::POLICY: - typeName = tr("Policy"); - break; - } - - const auto &progress = model.getArtifact().getDownloadProgress(); - QString line; - - if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING) { - line = tr("Downloading %1 model [%2]... (%3%)").arg(typeName, modelName).arg(progress.getProgress(), 0, 'f', 2); - } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADED) { - line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("downloaded") : tr("ready")); - } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::CACHED) { - line = tr("%1 model [%2] %3").arg(typeName, modelName, download_status_changed ? tr("from cache") : tr("ready")); - } else if (progress.getStatus() == cereal::ModelManagerSP::DownloadStatus::FAILED) { - line = tr("%1 model [%2] download failed").arg(typeName, modelName); - } else { - line = tr("%1 model [%2] pending...").arg(typeName, modelName); - } - status.append(line); - } - - currentModelLblBtn->setDescription(status.join("\n")); - - if (prev_download_status != download_status) { - switch (bundle.getStatus()) { - case cereal::ModelManagerSP::DownloadStatus::DOWNLOADING: - case cereal::ModelManagerSP::DownloadStatus::CACHED: - case cereal::ModelManagerSP::DownloadStatus::DOWNLOADED: - currentModelLblBtn->showDescription(); - break; - case cereal::ModelManagerSP::DownloadStatus::FAILED: - default: - break; - } - } - prev_download_status = download_status; -} - -/** - * @brief Gets the name of the currently selected model bundle - * @return Display name of the selected bundle or default model name - */ -QString SoftwarePanelSP::GetActiveModelName() { - if (model_manager.hasActiveBundle()) { - return QString::fromStdString(model_manager.getActiveBundle().getDisplayName()); - } - - return DEFAULT_MODEL; -} - -void SoftwarePanelSP::updateModelManagerState() { - const SubMaster &sm = *(uiStateSP()->sm); - model_manager = sm["modelManagerSP"].getModelManagerSP(); -} - -/** - * @brief Handles the model bundle selection button click - * Displays available bundles, allows selection, and initiates download - */ -void SoftwarePanelSP::handleCurrentModelLblBtnClicked() { - currentModelLblBtn->setEnabled(false); - currentModelLblBtn->setValue(tr("Fetching models...")); - - // Create mapping of bundle indices to display names - QMap index_to_bundle; - const auto bundles = model_manager.getAvailableBundles(); - for (const auto &bundle: bundles) { - index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundle.getDisplayName())); - } - - // Sort bundles by index in descending order - QStringList bundleNames; - // Add "Default" as the first option - bundleNames.append(tr("Use Default")); - - auto indices = index_to_bundle.keys(); - std::sort(indices.begin(), indices.end(), std::greater()); - for (const auto &index: indices) { - bundleNames.append(index_to_bundle[index]); - } - - currentModelLblBtn->setValue(GetActiveModelName()); - - const QString selectedBundleName = MultiOptionDialog::getSelection( - tr("Select a Model"), bundleNames, GetActiveModelName(), this); - - if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) { - return; - } - - // Handle "Stock" selection differently - if (selectedBundleName == tr("Use Default")) { - params.remove("ModelManager_ActiveBundle"); - currentModelLblBtn->setValue(tr("Default")); - showResetParamsDialog(); - } else { - // Find selected bundle and initiate download - for (const auto &bundle: bundles) { - if (QString::fromStdString(bundle.getDisplayName()) == selectedBundleName) { - params.put("ModelManager_DownloadIndex", std::to_string(bundle.getIndex())); - if (bundle.getGeneration() != model_manager.getActiveBundle().getGeneration()) { - showResetParamsDialog(); - } - break; + // branch selector + QObject::disconnect(targetBranchBtn, nullptr, nullptr, nullptr); + connect(targetBranchBtn, &ButtonControlSP::clicked, [=]() { + InputDialog d(tr("Search Branch"), this, tr("Enter search keywords, or leave blank to list all branches."), false); + d.setMinLength(0); + const int ret = d.exec(); + if (ret) { + searchBranches(d.text()); } - } - } - - updateLabels(); + }); } /** - * @brief Updates the UI elements based on current state + * @brief Searches for available branches based on a query string, presents the results in a dialog, + * and updates the target branch if a selection is made. + * + * This function filters the list of branches based on the provided query, and displays the filtered branches in a selection dialog. + * If a branch is selected, the "UpdaterTargetBranch" parameter is updated and a check for updates is triggered. + * If no branches are found matching the query, an alert dialog is displayed. + * + * @param query The search query string. */ -void SoftwarePanelSP::updateLabels() { - if (!isVisible()) { +void SoftwarePanelSP::searchBranches(const QString &query) { + + QStringList branches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(","); + QStringList results = searchFromList(query, branches); + results.sort(); + + if (results.isEmpty()) { + ConfirmationDialog::alert(tr("No branches found for keywords: %1").arg(query), this); return; } - updateModelManagerState(); - handleBundleDownloadProgress(); - currentModelLblBtn->setEnabled(!is_onroad && !isDownloading()); - currentModelLblBtn->setValue(GetActiveModelName()); + QString selected_branch = MultiOptionDialog::getSelection(tr("Select a branch"), results, "", this); - SoftwarePanel::updateLabels(); -} - -/** - * @brief Shows dialog prompting user to reset calibration after model download - */ -void SoftwarePanelSP::showResetParamsDialog() { - const auto confirmMsg = QString("%1

%2

%3") - .arg(tr("Model download has started in the background.")) - .arg(tr("We STRONGLY suggest you to reset calibration.")) - .arg(tr("Would you like to do that now?")); - const auto button_text = tr("Reset Calibration"); - - QString content("

" + tr("Driving Model Selector") + "


" - "

" + confirmMsg + "

"); - - if (showConfirmationDialog(content, button_text, false)) { - params.remove("CalibrationParams"); - params.remove("LiveTorqueParameters"); + if (!selected_branch.isEmpty()) { + params.put("UpdaterTargetBranch", selected_branch.toStdString()); + targetBranchBtn->setValue(selected_branch); + checkForUpdates(); } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h index d0299d6efd..56d8b9be5b 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h @@ -7,8 +7,8 @@ #pragma once -#include #include "selfdrive/ui/sunnypilot/ui.h" +#include "selfdrive/ui/sunnypilot/qt/util.h" #include "selfdrive/ui/qt/offroad/settings.h" class SoftwarePanelSP final : public SoftwarePanel { @@ -18,45 +18,5 @@ public: explicit SoftwarePanelSP(QWidget *parent = nullptr); private: - QString GetActiveModelName(); - void updateModelManagerState(); - - bool isDownloading() const { - if (!model_manager.hasSelectedBundle()) { - return false; - } - - const auto &selected_bundle = model_manager.getSelectedBundle(); - return selected_bundle.getStatus() == cereal::ModelManagerSP::DownloadStatus::DOWNLOADING; - } - - // UI update related methods - void updateLabels() override; - void handleCurrentModelLblBtnClicked(); - void handleBundleDownloadProgress(); - void showResetParamsDialog(); - cereal::ModelManagerSP::Reader model_manager; - cereal::ModelManagerSP::DownloadStatus download_status{}; - cereal::ModelManagerSP::DownloadStatus prev_download_status{}; - - bool canContinueOnMeteredDialog() { - if (!is_metered) return true; - return showConfirmationDialog(QString(), QString(), is_metered); - } - - inline bool showConfirmationDialog(const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) { - return showConfirmationDialog(this, message, confirmButtonText, show_metered_warning); - } - - static inline bool showConfirmationDialog(QWidget *parent, const QString &message = QString(), const QString &confirmButtonText = QString(), const bool show_metered_warning = false) { - const QString warning_message = show_metered_warning ? tr("Warning: You are on a metered connection!") : QString(); - const QString final_message = QString("%1%2").arg(!message.isEmpty() ? message + "\n" : QString(), warning_message); - const QString final_buttonText = !confirmButtonText.isEmpty() ? confirmButtonText : QString(tr("Continue") + " %1").arg(show_metered_warning ? tr("on Metered") : ""); - - return ConfirmationDialog(final_message, final_buttonText, tr("Cancel"), true, parent).exec(); - } - - bool is_metered{}; - bool is_wifi{}; - ButtonControlSP *currentModelLblBtn; + void searchBranches(const QString &query); }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc new file mode 100644 index 0000000000..0d42bd74f4 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h" + +VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { + param_watcher = new ParamWatcher(this); + connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { + paramsRefresh(); + }); + + main_layout = new QStackedLayout(this); + ListWidgetSP *list = new ListWidgetSP(this, false); + + sunnypilotScreen = new QWidget(this); + QVBoxLayout* vlayout = new QVBoxLayout(sunnypilotScreen); + vlayout->setContentsMargins(50, 20, 50, 20); + + std::vector > toggle_defs{ + { + "BlindSpot", + tr("Show Blind Spot Warnings"), + tr("Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported."), + "../assets/offroad/icon_monitoring.png", + false, + }, + }; + + for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) { + auto toggle = new ParamControlSP(param, title, desc, icon, this); + + bool locked = params.getBool((param + "Lock").toStdString()); + toggle->setEnabled(!locked); + + if (needs_restart && !locked) { + toggle->setDescription(toggle->getDescription() + tr(" Changing this setting will restart openpilot if the car is powered on.")); + + QObject::connect(uiState(), &UIState::engagedChanged, [toggle](bool engaged) { + toggle->setEnabled(!engaged); + }); + + QObject::connect(toggle, &ParamControlSP::toggleFlipped, [=](bool state) { + params.putBool("OnroadCycleRequested", true); + }); + } + + list->addItem(toggle); + toggles[param.toStdString()] = toggle; + param_watcher->addParam(param); + } + + sunnypilotScroller = new ScrollViewSP(list, this); + vlayout->addWidget(sunnypilotScroller); + + main_layout->addWidget(sunnypilotScreen); +} + +void VisualsPanel::paramsRefresh() { + if (!isVisible()) { + return; + } + + for (auto toggle : toggles) { + toggle.second->refresh(); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h new file mode 100644 index 0000000000..42e0688957 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" + +class ScrollViewSP; + +class VisualsPanel : public QWidget { + Q_OBJECT + +public: + explicit VisualsPanel(QWidget *parent = nullptr); + + void paramsRefresh(); + +protected: + QStackedLayout* main_layout = nullptr; + QWidget* sunnypilotScreen = nullptr; + ScrollViewSP *sunnypilotScroller = nullptr; + Params params; + std::map toggles; + ParamWatcher * param_watcher; +}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/model.cc b/selfdrive/ui/sunnypilot/qt/onroad/model.cc index 617b64f58e..af0177c344 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/model.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/model.cc @@ -6,3 +6,47 @@ */ #include "selfdrive/ui/sunnypilot/qt/onroad/model.h" + + +void ModelRendererSP::update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) { + ModelRenderer::update_model(model, lead); + const auto &model_position = model.getPosition(); + const auto &lane_lines = model.getLaneLines(); + float max_distance = std::clamp(*(model_position.getX().end() - 1), MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE); + int max_idx = get_path_length_idx(lane_lines[0], max_distance); + // update blindspot vertices + float max_distance_barrier = 100; + int max_idx_barrier = std::min(max_idx, get_path_length_idx(lane_lines[0], max_distance_barrier)); + mapLineToPolygon(model.getLaneLines()[1], 0.2, -0.05, &left_blindspot_vertices, max_idx_barrier); + mapLineToPolygon(model.getLaneLines()[2], 0.2, -0.05, &right_blindspot_vertices, max_idx_barrier); +} + +void ModelRendererSP::drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &surface_rect) { + auto *s = uiState(); + auto &sm = *(s->sm); + bool blindspot = Params().getBool("BlindSpot"); + + if (blindspot) { + bool left_blindspot = sm["carState"].getCarState().getLeftBlindspot(); + bool right_blindspot = sm["carState"].getCarState().getRightBlindspot(); + + //painter.setBrush(QColor::fromRgbF(1.0, 0.0, 0.0, 0.4)); // Red with alpha for blind spot + + if (left_blindspot && !left_blindspot_vertices.isEmpty()) { + QLinearGradient gradient(0, 0, surface_rect.width(), 0); // Horizontal gradient from left to right + gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha + gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha + painter.setBrush(gradient); + painter.drawPolygon(left_blindspot_vertices); + } + + if (right_blindspot && !right_blindspot_vertices.isEmpty()) { + QLinearGradient gradient(surface_rect.width(), 0, 0, 0); // Horizontal gradient from right to left + gradient.setColorAt(0.0, QColor(255, 165, 0, 102)); // Orange with alpha + gradient.setColorAt(1.0, QColor(255, 255, 0, 102)); // Yellow with alpha + painter.setBrush(gradient); + painter.drawPolygon(right_blindspot_vertices); + } + } + ModelRenderer::drawPath(painter, model, surface_rect.height()); +} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/model.h b/selfdrive/ui/sunnypilot/qt/onroad/model.h index e8594629cb..24404f32f0 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/model.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/model.h @@ -11,5 +11,12 @@ class ModelRendererSP : public ModelRenderer { public: - ModelRendererSP() {} + ModelRendererSP() = default; + +private: + void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead) override; + void drawPath(QPainter &painter, const cereal::ModelDataV2::Reader &model, const QRect &rect) override; + + QPolygonF left_blindspot_vertices; + QPolygonF right_blindspot_vertices; }; diff --git a/selfdrive/ui/sunnypilot/qt/util.cc b/selfdrive/ui/sunnypilot/qt/util.cc index c729eaafd5..ca85935d0b 100644 --- a/selfdrive/ui/sunnypilot/qt/util.cc +++ b/selfdrive/ui/sunnypilot/qt/util.cc @@ -78,3 +78,35 @@ QMap loadPlatformList() { return _platforms; } + +/** + * @brief Searches a list of strings for elements containing all search terms in a query. + * + * The search is case-insensitive and normalizes both the query and the list elements + * using Unicode KD normalization before comparison. Non-alphanumeric characters are + * removed from the search terms before comparison. + * + * @param query The search query string. Multiple words can be separated by spaces. + * @param list The source list of strings to search. + * @return A list of strings from the input list that contain all of the search terms. + */ +QStringList searchFromList(const QString &query, const QStringList &list) { + if (query.isEmpty()) { + return list; + } + + QStringList search_terms = query.simplified().toLower().split(" ", QString::SkipEmptyParts); + QStringList search_results; + + for (const QString &element : list) { + if (std::all_of(search_terms.begin(), search_terms.end(), [&](const QString &term) { + QString normalized_term = term.normalized(QString::NormalizationForm_KD).toLower(); + normalized_term.remove(QRegularExpression("[^a-zA-Z0-9\\s]")); + QString normalized_element = element.normalized(QString::NormalizationForm_KD).toLower(); + return normalized_element.contains(normalized_term, Qt::CaseInsensitive); + })) { + search_results << element; + } + } + return search_results; +} diff --git a/selfdrive/ui/sunnypilot/qt/util.h b/selfdrive/ui/sunnypilot/qt/util.h index 0a581579e2..089b5370cc 100644 --- a/selfdrive/ui/sunnypilot/qt/util.h +++ b/selfdrive/ui/sunnypilot/qt/util.h @@ -12,9 +12,11 @@ #include #include +#include #include QString getUserAgent(bool sunnylink = false); std::optional getSunnylinkDongleId(); std::optional getParamIgnoringDefault(const std::string ¶m_name, const std::string &default_value); QMap loadPlatformList(); +QStringList searchFromList(const QString &query, const QStringList &list); diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index e31c4047e9..c8849faf53 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -247,14 +247,11 @@ public: if (inline_layout) { button_param_layout->setMargin(0); button_param_layout->setSpacing(0); + spacingItem = nullptr; if (!title.isEmpty()) { main_layout->removeWidget(title_label); hlayout->addWidget(title_label, 1); } - if (spacingItem != nullptr && main_layout->indexOf(spacingItem) != -1) { - main_layout->removeItem(spacingItem); - spacingItem = nullptr; - } } button_group = new QButtonGroup(this); diff --git a/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.cc b/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.cc index 032abdbbfb..bfdcac2564 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.cc +++ b/selfdrive/ui/sunnypilot/qt/widgets/drive_stats.cc @@ -34,7 +34,7 @@ DriveStats::DriveStats(QWidget* parent) : QFrame(parent) { int row = 0; grid_layout->addWidget(newLabel(title, "title"), row++, 0, 1, 3); - grid_layout->addItem(new QSpacerItem(0, 50), row++, 0, 1, 1); + grid_layout->addItem(new QSpacerItem(0, 30), row++, 0, 1, 1); grid_layout->addWidget(labels.routes = newLabel("0", "number"), row, 0, Qt::AlignLeft); grid_layout->addWidget(labels.distance = newLabel("0", "number"), row, 1, Qt::AlignLeft); diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc index 586a03b1dd..b2701356cc 100644 --- a/selfdrive/ui/sunnypilot/ui.cc +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -41,6 +41,7 @@ void UIStateSP::update() { DeviceSP::DeviceSP(QObject *parent) : Device(parent) { QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &DeviceSP::update); + QObject::connect(this, &Device::displayPowerChanged, this, &DeviceSP::handleDisplayPowerChanged); } UIStateSP *uiStateSP() { @@ -62,3 +63,10 @@ DeviceSP *deviceSP() { static DeviceSP _device; return &_device; } + +void DeviceSP::handleDisplayPowerChanged(bool on) { + // if enabled, trigger offroad mode when device goes to sleep + if (params.get("DeviceBootMode") == "1" && not on) { + params.putBool("OffroadMode", true); + } +} diff --git a/selfdrive/ui/sunnypilot/ui.h b/selfdrive/ui/sunnypilot/ui.h index 08e1d37b0f..cf8de1c4bb 100644 --- a/selfdrive/ui/sunnypilot/ui.h +++ b/selfdrive/ui/sunnypilot/ui.h @@ -20,6 +20,11 @@ class UIStateSP : public UIState { public: UIStateSP(QObject *parent = 0); void updateStatus() override; + inline bool engaged() const override { + return scene.started && ( + (*sm)["selfdriveState"].getSelfdriveState().getEnabled() || (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads().getEnabled() + ); + } void setSunnylinkRoles(const std::vector &roles); void setSunnylinkDeviceUsers(const std::vector &users); @@ -79,6 +84,10 @@ class DeviceSP : public Device { public: DeviceSP(QObject *parent = 0); + +private: + Params params; + void handleDisplayPowerChanged(bool on); }; DeviceSP *deviceSP(); diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 9d078fdd9e..b263e28297 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -26,7 +26,7 @@ from openpilot.tools.lib.framereader import FrameReader from openpilot.tools.lib.route import Route from openpilot.tools.lib.cache import DEFAULT_CACHE_DIR -UI_DELAY = 0.1 # may be slower on CI? +UI_DELAY = 0.5 # may be slower on CI? TEST_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" STREAMS: list[tuple[VisionStreamType, CameraConfig, bytes]] = [] @@ -213,6 +213,12 @@ def setup_settings_sunnylink_sponsor_button(click, pm: PubMaster, scroll=None): click(1967, 225) time.sleep(UI_DELAY) +def setup_settings_models(click, pm: PubMaster, scroll=None): + setup_settings_device(click, pm) + click(278, 852) + time.sleep(UI_DELAY) + + def setup_settings_steering(click, pm: PubMaster, scroll=None): CP = car.CarParams() CP.carFingerprint = "HONDA_CIVIC" @@ -223,26 +229,33 @@ def setup_settings_steering(click, pm: PubMaster, scroll=None): Params().put("CarParamsSPPersistent", CP_SP.to_bytes()) setup_settings_device(click, pm) - click(278, 852) + click(278, 962) time.sleep(UI_DELAY) def setup_settings_steering_mads(click, pm: PubMaster, scroll=None): Params().put_bool("Mads", True) setup_settings_device(click, pm) - click(278, 852) + click(278, 962) click(970, 250) time.sleep(UI_DELAY) def setup_settings_steering_alc(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) - click(278, 852) + click(278, 962) click(970, 534) time.sleep(UI_DELAY) -def setup_settings_driving(click, pm: PubMaster, scroll=None): +def setup_settings_cruise(click, pm: PubMaster, scroll=None): setup_settings_device(click, pm) - click(278, 962) + scroll(-400, 278, 962) + click(278, 324) + time.sleep(UI_DELAY) + +def setup_settings_visuals(click, pm: PubMaster, scroll=None): + setup_settings_device(click, pm) + scroll(-400, 278, 962) + click(278, 560) time.sleep(UI_DELAY) def setup_settings_trips(click, pm: PubMaster, scroll=None): @@ -295,10 +308,12 @@ CASES = { CASES.update({ "settings_sunnylink": setup_settings_sunnylink, "settings_sunnylink_sponsor_button": setup_settings_sunnylink_sponsor_button, + "settings_models": setup_settings_models, "settings_steering": setup_settings_steering, "settings_steering_mads": setup_settings_steering_mads, "settings_steering_alc": setup_settings_steering_alc, - "settings_driving": setup_settings_driving, + "settings_cruise": setup_settings_cruise, + "settings_visuals": setup_settings_visuals, "settings_trips": setup_settings_trips, "settings_vehicle": setup_settings_vehicle, }) @@ -376,9 +391,9 @@ def create_screenshots(): driver_img = frames[2] else: with open(frames_cache, 'wb') as f: - road_img = FrameReader(route.camera_paths()[segnum]).get(0, pix_fmt="nv12")[0] - wide_road_img = FrameReader(route.ecamera_paths()[segnum]).get(0, pix_fmt="nv12")[0] - driver_img = FrameReader(route.dcamera_paths()[segnum]).get(0, pix_fmt="nv12")[0] + road_img = FrameReader(route.camera_paths()[segnum], pix_fmt="nv12").get(0) + wide_road_img = FrameReader(route.ecamera_paths()[segnum], pix_fmt="nv12").get(0) + driver_img = FrameReader(route.dcamera_paths()[segnum], pix_fmt="nv12").get(0) pickle.dump([road_img, wide_road_img, driver_img], f) STREAMS.append((VisionStreamType.VISION_STREAM_ROAD, cam.fcam, road_img.flatten().tobytes())) diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index 751ae7ce9b..de24097d8f 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -62,10 +62,6 @@ Cellular Metered محدود بالاتصال الخلوي - - Prevent large data uploads when on a metered connection - منع تحميل البيانات الكبيرة عندما يكون الاتصال محدوداً - Hidden Network شبكة مخفية @@ -86,6 +82,30 @@ for "%1" من أجل "%1" + + Prevent large data uploads when on a metered cellular connection + + + + default + + + + metered + + + + unmetered + + + + Wi-Fi Network Metered + + + + Prevent large data uploads when on a metered Wi-Fi connection + + AutoLaneChangeTimer @@ -151,18 +171,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode وضع المناورة الطولية - - sunnypilot Longitudinal Control (Alpha) - التحكم الطولي sunnypilot (ألفا) - - - WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - تحذير: التحكم الطولي في sunnypilot في المرحلة ألفا لهذه السيارة، وسيقوم بتعطيل مكابح الطوارئ الآلية (AEB). - - - On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. - في هذه السيارة يعمل sunnypilot افتراضياً بالشكل المدمج في التحكم التكيفي في السرعة بدلاً من التحكم الطولي. قم بتمكين هذا الخيار من أجل الانتقال إلى التحكم الطولي. يوصى بتمكين الوضع التجريبي عند استخدام وضع التحكم الطولي ألفا من sunnypilot. - Enable ADB تمكين ADB @@ -171,14 +179,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. أداة ADB (Android Debug Bridge) تسمح بالاتصال بجهازك عبر USB أو عبر الشبكة. راجع هذا الرابط: https://docs.comma.ai/how-to/connect-to-comma لمزيد من المعلومات. - - Hyundai: Enable Radar Tracks - - - - Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. - - Enable GitHub runner service @@ -199,6 +199,18 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + DevicePanel @@ -342,6 +354,14 @@ Please use caution when using this feature. Only use the blinker when traffic an PAIR إقران + + Disengage to Reset Calibration + + + + Resetting calibration will restart openpilot if the car is powered on. + + DevicePanelSP @@ -530,6 +550,49 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX + + HyundaiSettings + + Off + + + + Dynamic + + + + Predictive + + + + Custom Longitudinal Tuning + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. + + + + Off: Uses default tuning + + + + Dynamic: Adjusts acceleration limits based on current speed + + + + Predictive: Uses future trajectory data to anticipate needed adjustments + + + + Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. + + + InputDialog @@ -548,13 +611,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - Installer - - Installing... - جارٍ التثبيت... - - LaneChangeSettings @@ -588,6 +644,22 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). + + + + Start the vehicle to check vehicle compatibility. + + + + This platform supports all MADS settings. + + + + This platform supports limited MADS settings. + + MadsSettings @@ -615,10 +687,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remain Active - - Pause Steering - - Disengage @@ -628,12 +696,179 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - -Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused when the brake pedal is manually pressed. + Start the vehicle to check vehicle compatibility. + + This feature defaults to OFF, and does not allow selection due to vehicle limitations. + + + + This feature defaults to ON, and does not allow selection due to vehicle limitations. + + + + This platform only supports Disengage mode due to vehicle limitations. + + + + Remain Active: ALC will remain active when the brake pedal is pressed. + + + + Pause + + + + Pause: ALC will pause when the brake pedal is pressed. + + + + Disengage: ALC will disengage when the brake pedal is pressed. + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + + + + + MaxTimeOffroad + + Max Time Offroad + + + + Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) + + + + Always On + + + + h + + + + m + + + + (default) + + + + + ModelsPanel + + Current Model + + + + SELECT + اختيار + + + No custom model selected! + + + + Driving + + + + Navigation + + + + Vision + + + + Policy + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] %3 + + + + downloaded + + + + ready + + + + from cache + + + + %1 model [%2] download failed + + + + %1 model [%2] pending... + + + + Fetching models... + + + + Use Default + + + + Select a Model + + + + Default + + + + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + إعادة ضبط المعايرة + + + Driving Model Selector + + + + Warning: You are on a metered connection! + + + + Continue + متابعة + + + on Metered + + + + Cancel + إلغاء + MultiOptionDialog @@ -925,6 +1160,30 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed.Select a vehicle + + Unrecognized Vehicle + + + + Fingerprinted automatically + + + + Manually selected + + + + Not fingerprinted or manually selected + + + + Select vehicle to force fingerprint manually. + + + + Colors represent fingerprint status: + + PrimeAdWidget @@ -970,14 +1229,6 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed. QObject - - Reboot - إعادة التشغيل - - - Exit - إغلاق - sunnypilot sunnypilot @@ -1138,6 +1389,18 @@ This may take up to a minute. Steering + + Models + + + + Cruise + + + + Visuals + + Setup @@ -1429,108 +1692,20 @@ This may take up to a minute. SoftwarePanelSP - Current Model + Search Branch - SELECT - اختيار - - - No custom model selected! + Enter search keywords, or leave blank to list all branches. - Driving + No branches found for keywords: %1 - Navigation - - - - Metadata - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - إعادة ضبط المعايرة - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - متابعة - - - on Metered - - - - Cancel - إلغاء + Select a branch + اختر فرعاً @@ -1834,10 +2009,6 @@ This may take up to a minute. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. الوضع التجريبي غير متوفر حالياً في هذه السيارة نظراً لاستخدام رصيد التحكم التكيفي بالسرعة من أجل التحكم الطولي. - - sunnypilot longitudinal control may come in a future update. - قد يتم الحصول على التحكم الطولي في sunnypilot في عمليات التحديث المستقبلية. - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. يمكن اختبار نسخة ألفا من التحكم الطولي من sunnypilot، مع الوضع التجريبي، لكن على الفروع غير المطلقة. @@ -1875,8 +2046,16 @@ This may take up to a minute. تمكين sunnypilot - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - استخدم نظام sunnypilot من أجل الضبط التكيفي للسرعة والحفاظ على مساعدة السائق للبقاء في المسار. انتباهك مطلوب في جميع الأوقات مع استخدام هذه الميزة. يعمل هذا التغيير في الإعدادات عند إيقاف تشغيل السيارة. + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + + + + Changing this setting will restart openpilot if the car is powered on. + + + + openpilot longitudinal control may come in a future update. + diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 3123e52578..6229920121 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -62,10 +62,6 @@ Cellular Metered Getaktete Verbindung - - Prevent large data uploads when on a metered connection - Hochladen großer Dateien über getaktete Verbindungen unterbinden - Hidden Network Verborgenes Netzwerk @@ -86,6 +82,58 @@ for "%1" für "%1" + + Prevent large data uploads when on a metered cellular connection + + + + default + + + + metered + + + + unmetered + + + + Wi-Fi Network Metered + + + + Prevent large data uploads when on a metered Wi-Fi connection + + + + + AutoLaneChangeTimer + + Auto Lane Change by Blinker + + + + Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. +Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. + + + + s + + + + Off + + + + Nudge + + + + Nudgeless + + ConfirmationDialog @@ -100,10 +148,6 @@ DeclinePage - - You must accept the Terms and Conditions in order to use openpilot. - Du musst die Nutzungsbedingungen akzeptieren, um Openpilot zu benutzen. - Back Zurück @@ -112,6 +156,10 @@ Decline, uninstall %1 Ablehnen, deinstallieren %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -131,10 +179,6 @@ WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). WARNUNG: Die openpilot Längsregelung befindet sich für dieses Fahrzeug im Alpha-Stadium und deaktiviert das automatische Notbremsen (AEB). - - On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - Bei diesem Fahrzeug verwendet openpilot standardmäßig den eingebauten Tempomaten anstelle der openpilot Längsregelung. Aktiviere diese Option, um auf die openpilot Längsregelung umzuschalten. Es wird empfohlen, den experimentellen Modus zu aktivieren, wenn die openpilot Längsregelung (Alpha) aktiviert wird. - Enable ADB ADB aktivieren @@ -143,6 +187,30 @@ ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB (Android Debug Bridge) ermöglicht die Verbindung zu deinem Gerät über USB oder Netzwerk. Siehe https://docs.comma.ai/how-to/connect-to-comma für weitere Informationen. + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + + + Enable GitHub runner service + + + + Enables or disables the github runner service. + + + + Error Log + + + + VIEW + ANSEHEN + + + View the error log for sunnypilot crashes. + + DevicePanel @@ -190,10 +258,6 @@ REVIEW TRAINING - - Review the rules, features, and limitations of openpilot - Wiederhole die Regeln, Fähigkeiten und Limitierungen von Openpilot - Are you sure you want to review the training guide? Bist du sicher, dass du die Trainingsanleitung wiederholen möchtest? @@ -226,10 +290,6 @@ Power Off Ausschalten - - openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. openpilot is continuously calibrating, resetting is rarely required. - Damit Openpilot funktioniert, darf die Installationsposition nicht mehr als 4° nach rechts/links, 5° nach oben und 9° nach unten abweichen. Openpilot kalibriert sich durchgehend, ein Zurücksetzen ist selten notwendig. - Your device is pointed %1° %2 and %3° %4. Deine Geräteausrichtung ist %1° %2 und %3° %4. @@ -286,6 +346,136 @@ PAIR KOPPELN + + Disengage to Reset Calibration + + + + Review the rules, features, and limitations of sunnypilot + + + + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. + + + + Resetting calibration will restart openpilot if the car is powered on. + + + + + DevicePanelSP + + Quiet Mode + + + + Driver Camera Preview + + + + Training Guide + + + + Regulatory + Rechtliche Hinweise + + + Language + + + + Reset Settings + + + + Are you sure you want to review the training guide? + Bist du sicher, dass du die Trainingsanleitung wiederholen möchtest? + + + Review + Überprüfen + + + Select a language + Sprache wählen + + + Reboot + Neustart + + + Power Off + Ausschalten + + + Offroad Mode + + + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + Bestätigen + + + Are you sure you want to enter Always Offroad mode? + + + + Disengage to Enter Always Offroad Mode + + + + Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. + + + + Reset + Zurücksetzen + + + The reset cannot be undone. You have been warned. + + + + Exit Always Offroad + + + + Always Offroad + + + + + DriveStats + + Drives + + + + Hours + + + + ALL TIME + + + + PAST WEEK + + + + KM + + + + Miles + + DriverViewWindow @@ -311,14 +501,6 @@ 🔥 Firehose Mode 🔥 🔥 Firehose-Modus 🔥 - - openpilot learns to drive by watching humans, like you, drive. - -Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - openpilot lernt das Fahren, indem es Menschen wie dir beim Fahren zuschaut. - -Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximieren, um die Fahrmodelle von openpilot zu verbessern. Mehr Daten bedeuten größere Modelle, was zu einem besseren Experimentellen Modus führt. - Firehose Mode: ACTIVE Firehose-Modus: AKTIV @@ -327,10 +509,6 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere ACTIVE AKTIV - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream openpilot (and particular forks) are able to be used for training. - Für maximale Effektivität bring dein Gerät jede Woche nach drinnen und verbinde es mit einem guten USB-C-Adapter und WLAN.<br><br>Der Firehose-Modus funktioniert auch während der Fahrt, wenn das Gerät mit einem Hotspot oder einer ungedrosselten SIM-Karte verbunden ist.<br><br><br><b>Häufig gestellte Fragen</b><br><br><i>Spielt es eine Rolle, wie oder wo ich fahre?</i> Nein, fahre einfach wie gewohnt.<br><br><i>Werden im Firehose-Modus alle meine Segmente hochgeladen?</i> Nein, wir wählen selektiv nur einen Teil deiner Segmente aus.<br><br><i>Welcher USB-C-Adapter ist gut?</i> Jedes Schnellladegerät für Handy oder Laptop sollte ausreichen.<br><br><i>Spielt es eine Rolle, welche Software ich nutze?</i> Ja, nur das offizielle Upstream‑openpilot (und bestimmte Forks) kann für das Training verwendet werden. - <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -342,6 +520,16 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INAKTIV</span>: Verbinde dich mit einem ungedrosselten Netzwerk + + sunnypilot learns to drive by watching humans, like you, drive. + +Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. + + + + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + + HudRenderer @@ -358,6 +546,49 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere MAX + + HyundaiSettings + + Off + + + + Dynamic + + + + Predictive + + + + Custom Longitudinal Tuning + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. + + + + Off: Uses default tuning + + + + Dynamic: Adjusts acceleration limits based on current speed + + + + Predictive: Uses future trajectory data to anticipate needed adjustments + + + + Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. + + + InputDialog @@ -373,10 +604,262 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere - Installer + LaneChangeSettings - Installing... - Installiere... + Back + Zurück + + + Auto Lane Change: Delay with Blind Spot + + + + Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. + + + + + LateralPanel + + Modular Assistive Driving System (MADS) + + + + Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. + + + + Customize MADS + + + + Customize Lane Change + + + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). + + + + Start the vehicle to check vehicle compatibility. + + + + This platform supports all MADS settings. + + + + This platform supports limited MADS settings. + + + + + MadsSettings + + Toggle with Main Cruise + + + + Unified Engagement Mode (UEM) + + + + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + + + + Engage lateral and longitudinal control with cruise control engagement. + + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + + + + Start the vehicle to check vehicle compatibility. + + + + This feature defaults to OFF, and does not allow selection due to vehicle limitations. + + + + This feature defaults to ON, and does not allow selection due to vehicle limitations. + + + + This platform only supports Disengage mode due to vehicle limitations. + + + + Remain Active + + + + Remain Active: ALC will remain active when the brake pedal is pressed. + + + + Pause + + + + Pause: ALC will pause when the brake pedal is pressed. + + + + Disengage + + + + Disengage: ALC will disengage when the brake pedal is pressed. + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + + + + + MaxTimeOffroad + + Max Time Offroad + + + + Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) + + + + Always On + + + + h + + + + m + + + + (default) + + + + + ModelsPanel + + Current Model + + + + SELECT + AUSWÄHLEN + + + No custom model selected! + + + + Driving + + + + Navigation + + + + Vision + + + + Policy + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] %3 + + + + downloaded + + + + ready + + + + from cache + + + + %1 model [%2] download failed + + + + %1 model [%2] pending... + + + + Fetching models... + + + + Use Default + + + + Select a Model + + + + Default + + + + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + Neu kalibrieren + + + Driving Model Selector + + + + Warning: You are on a metered connection! + + + + Continue + Fortsetzen + + + on Metered + + + + Cancel + Abbrechen @@ -409,16 +892,78 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere Falsches Passwort + + NetworkingSP + + Scan + + + + Scanning... + + + + + NeuralNetworkLateralControl + + Neural Network Lateral Control (NNLC) + + + + NNLC is currently not available on this platform. + + + + Start the car to check car compatibility + + + + NNLC Not Loaded + + + + NNLC Loaded + + + + Match + + + + Exact + + + + Fuzzy + + + + Match: "Exact" is ideal, but "Fuzzy" is fine too. + + + + Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. + + + + Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server + + + + with feedback, or to provide log data for your car if your car is currently unsupported: + + + + if there are any issues: + + + + and donate logs to get NNLC loaded for your car: + + + OffroadAlert - - Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won't engage in %1 - Stelle sofort eine Internetverbindung her, um nach Updates zu suchen. Wenn du keine Verbindung herstellst, kann openpilot in %1 nicht mehr aktiviert werden. - - - Connect to internet to check for updates. openpilot won't automatically start until it connects to internet to check for updates. - Verbinde dich mit dem Internet, um nach Updates zu suchen. openpilot startet nicht automatisch, bis eine Internetverbindung besteht und nach Updates gesucht wurde. - Unable to download updates %1 @@ -445,18 +990,30 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. Nicht unterstütztes NVMe-Laufwerk erkannt. Das Gerät kann dadurch deutlich mehr Strom verbrauchen und überhitzen. - - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. - openpilot konnte dein Auto nicht identifizieren. Dein Auto wird entweder nicht unterstützt oder die Steuergeräte (ECUs) werden nicht erkannt. Bitte reiche einen Pull Request ein, um die Firmware-Versionen für das richtige Fahrzeug hinzuzufügen. Hilfe findest du auf discord.comma.ai. - - - openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - openpilot hat eine Änderung der Montageposition des Geräts erkannt. Stelle sicher, dass das Gerät vollständig in der Halterung sitzt und die Halterung fest an der Windschutzscheibe befestigt ist. - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 Gerätetemperatur zu hoch. Das System kühlt ab, bevor es startet. Aktuelle interne Komponententemperatur: %1 + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + + + + sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. + + + + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + + + + sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + + OffroadHome @@ -475,10 +1032,6 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere OnroadAlerts - - openpilot Unavailable - openpilot nicht verfügbar - TAKE CONTROL IMMEDIATELY ÜBERNIMM SOFORT DIE KONTROLLE @@ -495,6 +1048,10 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere System Unresponsive System reagiert nicht + + sunnypilot Unavailable + + PairingPopup @@ -530,6 +1087,96 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere Aktivieren + + ParamControlSP + + Enable + Aktivieren + + + Cancel + Abbrechen + + + + PlatformSelector + + Vehicle + + + + SEARCH + + + + Search your vehicle + + + + Enter model year (e.g., 2021) and model name (Toyota Corolla): + + + + SEARCHING + + + + REMOVE + LÖSCHEN + + + This setting will take effect immediately. + + + + This setting will take effect once the device enters offroad state. + + + + Vehicle Selector + + + + Confirm + Bestätigen + + + Cancel + Abbrechen + + + No vehicles found for query: %1 + + + + Select a vehicle + + + + Unrecognized Vehicle + + + + Fingerprinted automatically + + + + Manually selected + + + + Not fingerprinted or manually selected + + + + Select vehicle to force fingerprint manually. + + + + Colors represent fingerprint status: + + + PrimeAdWidget @@ -574,10 +1221,6 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere QObject - - openpilot - openpilot - %n minute(s) ago @@ -603,6 +1246,10 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere now jetzt + + sunnypilot + + Reset @@ -676,6 +1323,65 @@ Dies kann bis zu einer Minute dauern. Firehose + + SettingsWindowSP + + × + x + + + Device + Gerät + + + Network + Netzwerk + + + sunnylink + + + + Toggles + Schalter + + + Software + Software + + + Models + + + + Steering + + + + Cruise + + + + Trips + + + + Vehicle + + + + Firehose + Firehose + + + Developer + Entwickler + + + Visuals + + + Setup @@ -762,14 +1468,14 @@ Dies kann bis zu einer Minute dauern. Choose Software to Install Wähle die zu installierende Software - - openpilot - openpilot - Custom Software Benutzerdefinierte Software + + sunnypilot + + SetupWidget @@ -862,6 +1568,33 @@ Dies kann bis zu einer Minute dauern. 5G + + SidebarSP + + DISABLED + + + + OFFLINE + OFFLINE + + + REGIST... + + + + ONLINE + ONLINE + + + ERROR + FEHLER + + + SUNNYLINK + + + SoftwarePanel @@ -938,6 +1671,25 @@ Dies kann bis zu einer Minute dauern. nie + + SoftwarePanelSP + + Search Branch + + + + Enter search keywords, or leave blank to list all branches. + + + + No branches found for keywords: %1 + + + + Select a branch + Wähle einen Branch + + SshControl @@ -984,6 +1736,168 @@ Dies kann bis zu einer Minute dauern. SSH aktivieren + + SunnylinkPanel + + This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. + + + + Enable sunnylink + + + + Sponsor Status + + + + SPONSOR + + + + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. + + + + Pair GitHub Account + + + + PAIR + KOPPELN + + + Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + + + N/A + Nicht verfügbar + + + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. + + + + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + + + + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + + + + Backup Settings + + + + Are you sure you want to backup sunnypilot settings? + + + + Back Up + + + + Restore Settings + + + + Are you sure you want to restore the last backed up sunnypilot settings? + + + + Restore + + + + Backup in progress %1% + + + + Backup Failed + + + + Settings backup completed. + + + + Restore in progress %1% + + + + Restore Failed + + + + Unable to restore the settings, try again later. + + + + Settings restored. Confirm to restart the interface. + + + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + + SunnylinkSponsorPopup + + Scan the QR code to login to your GitHub account + + + + Follow the prompts to complete the pairing process + + + + Re-enter the "sunnylink" panel to verify sponsorship status + + + + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot + + + + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page + + + + Choose your sponsorship tier and confirm your support + + + + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status + + + + Pair your GitHub account + + + + Early Access: Become a sunnypilot Sponsor + + + TermsPage @@ -995,24 +1909,16 @@ Dies kann bis zu einer Minute dauern. Zustimmen - Welcome to openpilot - Willkommen bei openpilot + Welcome to sunnypilot + - You must accept the Terms and Conditions to use openpilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - Du musst die Nutzungsbedingungen akzeptieren, um openpilot zu verwenden. Lies die aktuellen Bedingungen unter <span style='color: #465BEA;'>https://comma.ai/terms</span>, bevor du fortfährst. + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. + TogglesPanel - - Enable openpilot - Openpilot aktivieren - - - Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Benutze das Openpilot System als adaptiven Tempomaten und Spurhalteassistenten. Deine Aufmerksamkeit ist jederzeit erforderlich, um diese Funktion zu nutzen. Diese Einstellung wird übernommen, wenn das Auto aus ist. - Enable Lane Departure Warnings Spurverlassenswarnungen aktivieren @@ -1037,10 +1943,6 @@ Dies kann bis zu einer Minute dauern. Upload data from the driver facing camera and help improve the driver monitoring algorithm. Lade Daten der Fahreraufmerksamkeitsüberwachungskamera hoch, um die Fahreraufmerksamkeitsüberwachungsalgorithmen zu verbessern. - - When enabled, pressing the accelerator pedal will disengage openpilot. - Wenn aktiviert, deaktiviert sich Openpilot sobald das Gaspedal betätigt wird. - Experimental Mode Experimenteller Modus @@ -1049,14 +1951,6 @@ Dies kann bis zu einer Minute dauern. Disengage on Accelerator Pedal Bei Gasbetätigung ausschalten - - openpilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Openpilot fährt standardmäßig im <b>entspannten Modus</b>. Der Experimentelle Modus aktiviert<b>Alpha-level Funktionen</b>, die noch nicht für den entspannten Modus bereit sind. Die experimentellen Funktionen sind die Folgenden: - - - Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - Lass das Fahrmodell Gas und Bremse kontrollieren. Openpilot wird so fahren, wie es dies von einem Menschen erwarten würde; inklusive des Anhaltens für Ampeln und Stoppschildern. Da das Fahrmodell entscheidet wie schnell es fährt stellt die gesetzte Geschwindigkeit lediglich das obere Limit dar. Dies ist ein Alpha-level Funktion. Fehler sind zu erwarten. - New Driving Visualization Neue Fahrvisualisierung @@ -1089,18 +1983,6 @@ Dies kann bis zu einer Minute dauern. openpilot longitudinal control may come in a future update. Die openpilot Längsregelung könnte in einem zukünftigen Update verfügbar sein. - - An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Eine Alpha-Version der openpilot Längsregelung kann zusammen mit dem Experimentellen Modus auf non-stable Branches getestet werden. - - - Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode. - Aktiviere den Schalter für openpilot Längsregelung (Alpha), um den Experimentellen Modus zu erlauben. - - - Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Standard wird empfohlen. Im aggressiven Modus folgt openpilot vorausfahrenden Fahrzeugen enger und ist beim Gasgeben und Bremsen aggressiver. Im entspannten Modus hält openpilot mehr Abstand zu vorausfahrenden Fahrzeugen. Bei unterstützten Fahrzeugen kannst du mit der Abstandstaste am Lenkrad zwischen diesen Fahrstilen wechseln. - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. Die Fahrvisualisierung wechselt bei niedrigen Geschwindigkeiten auf die nach vorne gerichtete Weitwinkelkamera, um Kurven besser darzustellen. Das Logo des Experimentellen Modus wird außerdem oben rechts angezeigt. @@ -1110,8 +1992,52 @@ Dies kann bis zu einer Minute dauern. Dauerhaft aktive Fahrerüberwachung - Enable driver monitoring even when openpilot is not engaged. - Fahrerüberwachung auch aktivieren, wenn openpilot nicht aktiv ist. + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + + + + Enable Dynamic Experimental Control + + + + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + + + + When enabled, pressing the accelerator pedal will disengage sunnypilot. + + + + Enable driver monitoring even when sunnypilot is not engaged. + + + + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + + + + Changing this setting will restart openpilot if the car is powered on. + + + + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + + + + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 82f585a43c..3d9622166a 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -62,10 +62,6 @@ Cellular Metered Plano de datos limitado - - Prevent large data uploads when on a metered connection - Evitar grandes descargas de datos cuando tenga una conexión limitada - Hidden Network Red Oculta @@ -86,6 +82,30 @@ for "%1" para "%1" + + Prevent large data uploads when on a metered cellular connection + + + + default + + + + metered + + + + unmetered + + + + Wi-Fi Network Metered + + + + Prevent large data uploads when on a metered Wi-Fi connection + + AutoLaneChangeTimer @@ -151,18 +171,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode Modo de maniobra longitudinal - - sunnypilot Longitudinal Control (Alpha) - Control longitudinal de sunnypilot (fase experimental) - - - WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - AVISO: el control longitudinal de sunnypilot está en fase experimental para este automóvil y desactivará el Frenado Automático de Emergencia (AEB). - - - On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. - En este automóvil, sunnypilot se configura de manera predeterminada con el Autocrucero Adaptativo (ACC) incorporado en el automóvil en lugar del control longitudinal de sunnypilot. Habilita esta opción para cambiar al control longitudinal de sunnypilot. Se recomienda activar el modo experimental al habilitar el control longitudinal de sunnypilot (aún en fase experimental). - Enable ADB @@ -171,14 +179,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - Hyundai: Enable Radar Tracks - - - - Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. - - Enable GitHub runner service @@ -199,6 +199,18 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + DevicePanel @@ -342,6 +354,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Power Off Desactivar para apagar + + Disengage to Reset Calibration + + + + Resetting calibration will restart openpilot if the car is powered on. + + DevicePanelSP @@ -526,6 +546,49 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX + + HyundaiSettings + + Off + + + + Dynamic + + + + Predictive + + + + Custom Longitudinal Tuning + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. + + + + Off: Uses default tuning + + + + Dynamic: Adjusts acceleration limits based on current speed + + + + Predictive: Uses future trajectory data to anticipate needed adjustments + + + + Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. + + + InputDialog @@ -540,13 +603,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - Installer - - Installing... - Instalando... - - LaneChangeSettings @@ -580,6 +636,22 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). + + + + Start the vehicle to check vehicle compatibility. + + + + This platform supports all MADS settings. + + + + This platform supports limited MADS settings. + + MadsSettings @@ -607,10 +679,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remain Active - - Pause Steering - - Disengage @@ -620,12 +688,179 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - -Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused when the brake pedal is manually pressed. + Start the vehicle to check vehicle compatibility. + + This feature defaults to OFF, and does not allow selection due to vehicle limitations. + + + + This feature defaults to ON, and does not allow selection due to vehicle limitations. + + + + This platform only supports Disengage mode due to vehicle limitations. + + + + Remain Active: ALC will remain active when the brake pedal is pressed. + + + + Pause + + + + Pause: ALC will pause when the brake pedal is pressed. + + + + Disengage: ALC will disengage when the brake pedal is pressed. + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + + + + + MaxTimeOffroad + + Max Time Offroad + + + + Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) + + + + Always On + + + + h + + + + m + + + + (default) + + + + + ModelsPanel + + Current Model + + + + SELECT + SELECCIONAR + + + No custom model selected! + + + + Driving + + + + Navigation + + + + Vision + + + + Policy + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] %3 + + + + downloaded + + + + ready + + + + from cache + + + + %1 model [%2] download failed + + + + %1 model [%2] pending... + + + + Fetching models... + + + + Use Default + + + + Select a Model + + + + Default + + + + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + Formatear Calibración + + + Driving Model Selector + + + + Warning: You are on a metered connection! + + + + Continue + Continuar + + + on Metered + + + + Cancel + Cancelar + MultiOptionDialog @@ -917,6 +1152,30 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed.Select a vehicle + + Unrecognized Vehicle + + + + Fingerprinted automatically + + + + Manually selected + + + + Not fingerprinted or manually selected + + + + Select vehicle to force fingerprint manually. + + + + Colors represent fingerprint status: + + PrimeAdWidget @@ -962,14 +1221,6 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed. QObject - - Reboot - Reiniciar - - - Exit - Salir - sunnypilot sunnypilot @@ -1118,6 +1369,18 @@ Esto puede tardar un minuto. Steering + + Models + + + + Cruise + + + + Visuals + + Setup @@ -1409,108 +1672,20 @@ Esto puede tardar un minuto. SoftwarePanelSP - Current Model + Search Branch - SELECT - SELECCIONAR - - - No custom model selected! + Enter search keywords, or leave blank to list all branches. - Driving + No branches found for keywords: %1 - Navigation - - - - Metadata - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Formatear Calibración - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Continuar - - - on Metered - - - - Cancel - Cancelar + Select a branch + Selecione una rama @@ -1830,10 +2005,6 @@ Esto puede tardar un minuto. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. El modo Experimental no está disponible actualmente para este auto, ya que el ACC default del auto está siendo usado para el control longitudinal. - - sunnypilot longitudinal control may come in a future update. - El control longitudinal de sunnypilot podrá llegar en futuras actualizaciones. - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. Se puede probar una versión experimental del control longitudinal sunnypilot, junto con el modo Experimental, en ramas no liberadas. @@ -1855,8 +2026,16 @@ Esto puede tardar un minuto. Activar sunnypilot - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Utilice el sistema sunnypilot para acceder a un autocrucero adaptativo y asistencia al conductor para mantenerse en el carril. Se requiere su atención en todo momento para utilizar esta función. Cambiar esta configuración solo tendrá efecto con el auto apagado. + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + + + + Changing this setting will restart openpilot if the car is powered on. + + + + openpilot longitudinal control may come in a future update. + diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index fba7af60d9..741cc606a7 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -62,10 +62,6 @@ Cellular Metered Connexion cellulaire limitée - - Prevent large data uploads when on a metered connection - Éviter les transferts de données importants sur une connexion limitée - Hidden Network Réseau Caché @@ -86,6 +82,30 @@ for "%1" pour "%1" + + Prevent large data uploads when on a metered cellular connection + + + + default + + + + metered + + + + unmetered + + + + Wi-Fi Network Metered + + + + Prevent large data uploads when on a metered Wi-Fi connection + + AutoLaneChangeTimer @@ -151,18 +171,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode Mode manœuvre longitudinale - - sunnypilot Longitudinal Control (Alpha) - Contrôle longitudinal sunnypilot (Alpha) - - - WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - ATTENTION : le contrôle longitudinal sunnypilot est en alpha pour cette voiture et désactivera le freinage d'urgence automatique (AEB). - - - On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. - Sur cette voiture, sunnypilot utilise par défaut le régulateur de vitesse adaptatif intégré à la voiture plutôt que le contrôle longitudinal d'sunnypilot. Activez ceci pour passer au contrôle longitudinal sunnypilot. Il est recommandé d'activer le mode expérimental lors de l'activation du contrôle longitudinal sunnypilot alpha. - Enable ADB @@ -171,14 +179,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - Hyundai: Enable Radar Tracks - - - - Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. - - Enable GitHub runner service @@ -199,6 +199,18 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + DevicePanel @@ -342,6 +354,14 @@ Please use caution when using this feature. Only use the blinker when traffic an PAIR ASSOCIER + + Disengage to Reset Calibration + + + + Resetting calibration will restart openpilot if the car is powered on. + + DevicePanelSP @@ -526,6 +546,49 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX + + HyundaiSettings + + Off + + + + Dynamic + + + + Predictive + + + + Custom Longitudinal Tuning + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. + + + + Off: Uses default tuning + + + + Dynamic: Adjusts acceleration limits based on current speed + + + + Predictive: Uses future trajectory data to anticipate needed adjustments + + + + Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. + + + InputDialog @@ -540,13 +603,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - Installer - - Installing... - Installation... - - LaneChangeSettings @@ -580,6 +636,22 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). + + + + Start the vehicle to check vehicle compatibility. + + + + This platform supports all MADS settings. + + + + This platform supports limited MADS settings. + + MadsSettings @@ -607,10 +679,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remain Active - - Pause Steering - - Disengage @@ -620,12 +688,179 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - -Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused when the brake pedal is manually pressed. + Start the vehicle to check vehicle compatibility. + + This feature defaults to OFF, and does not allow selection due to vehicle limitations. + + + + This feature defaults to ON, and does not allow selection due to vehicle limitations. + + + + This platform only supports Disengage mode due to vehicle limitations. + + + + Remain Active: ALC will remain active when the brake pedal is pressed. + + + + Pause + + + + Pause: ALC will pause when the brake pedal is pressed. + + + + Disengage: ALC will disengage when the brake pedal is pressed. + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + + + + + MaxTimeOffroad + + Max Time Offroad + + + + Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) + + + + Always On + + + + h + + + + m + + + + (default) + + + + + ModelsPanel + + Current Model + + + + SELECT + SÉLECTIONNER + + + No custom model selected! + + + + Driving + + + + Navigation + + + + Vision + + + + Policy + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] %3 + + + + downloaded + + + + ready + + + + from cache + + + + %1 model [%2] download failed + + + + %1 model [%2] pending... + + + + Fetching models... + + + + Use Default + + + + Select a Model + + + + Default + + + + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + Réinitialiser la calibration + + + Driving Model Selector + + + + Warning: You are on a metered connection! + + + + Continue + Continuer + + + on Metered + + + + Cancel + Annuler + MultiOptionDialog @@ -917,6 +1152,30 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed.Select a vehicle + + Unrecognized Vehicle + + + + Fingerprinted automatically + + + + Manually selected + + + + Not fingerprinted or manually selected + + + + Select vehicle to force fingerprint manually. + + + + Colors represent fingerprint status: + + PrimeAdWidget @@ -962,14 +1221,6 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed. QObject - - Reboot - Redémarrer - - - Exit - Quitter - sunnypilot sunnypilot @@ -1118,6 +1369,18 @@ Cela peut prendre jusqu'à une minute. Steering + + Models + + + + Cruise + + + + Visuals + + Setup @@ -1409,108 +1672,20 @@ Cela peut prendre jusqu'à une minute. SoftwarePanelSP - Current Model + Search Branch - SELECT - SÉLECTIONNER - - - No custom model selected! + Enter search keywords, or leave blank to list all branches. - Driving + No branches found for keywords: %1 - Navigation - - - - Metadata - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Réinitialiser la calibration - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Continuer - - - on Metered - - - - Cancel - Annuler + Select a branch + Sélectionner une branche @@ -1810,10 +1985,6 @@ Cela peut prendre jusqu'à une minute. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. Le mode expérimental est actuellement indisponible pour cette voiture car le régulateur de vitesse adaptatif d'origine est utilisé pour le contrôle longitudinal. - - sunnypilot longitudinal control may come in a future update. - Le contrôle longitudinal sunnypilot pourrait être disponible dans une future mise à jour. - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. Une version alpha du contrôle longitudinal sunnypilot peut être testée, avec le mode expérimental, sur des branches non publiées. @@ -1855,8 +2026,16 @@ Cela peut prendre jusqu'à une minute. Activer sunnypilot - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Utilisez le système sunnypilot pour le régulateur de vitesse adaptatif et l'assistance au maintien de voie. Votre attention est requise en permanence pour utiliser cette fonctionnalité. La modification de ce paramètre prend effet lorsque la voiture est éteinte. + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + + + + Changing this setting will restart openpilot if the car is powered on. + + + + openpilot longitudinal control may come in a future update. + diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index dba867fcea..f85ee37689 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -62,10 +62,6 @@ Cellular Metered 従量制通信設定 - - Prevent large data uploads when on a metered connection - 大量のデータのアップロードを防止する - Hidden Network ネットワーク非表示 @@ -86,6 +82,30 @@ for "%1" [%1] + + Prevent large data uploads when on a metered cellular connection + + + + default + + + + metered + + + + unmetered + + + + Wi-Fi Network Metered + + + + Prevent large data uploads when on a metered Wi-Fi connection + + AutoLaneChangeTimer @@ -151,18 +171,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode アクセル制御マニューバー - - sunnypilot Longitudinal Control (Alpha) - sunnypilotアクセル制御(Alpha) - - - WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - この車ではsunnypilotのアクセル制御はアルファ版であり、自動緊急ブレーキ(AEB)が無効化されます。 - - - On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. - この車では、sunnypilotは車両内蔵のACC(アダプティブクルーズコントロール)をデフォルトとして使用し、sunnypilotのアクセル制御は無効化されています。アクセル制御をsunnypilotに切り替えるにはこの設定を有効にしてください。また同時にExperimentalモードを推奨します。 - Enable ADB ADBを有効にする @@ -171,14 +179,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android Debug Bridge)により、USBまたはネットワーク経由でデバイスに接続できます。詳細は、https://docs.comma.ai/how-to/connect-to-comma を参照してください。 - - Hyundai: Enable Radar Tracks - - - - Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. - - Enable GitHub runner service @@ -199,6 +199,18 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + DevicePanel @@ -342,6 +354,14 @@ Please use caution when using this feature. Only use the blinker when traffic an PAIR OK + + Disengage to Reset Calibration + + + + Resetting calibration will restart openpilot if the car is powered on. + + DevicePanelSP @@ -525,6 +545,49 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 最大速度 + + HyundaiSettings + + Off + + + + Dynamic + + + + Predictive + + + + Custom Longitudinal Tuning + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. + + + + Off: Uses default tuning + + + + Dynamic: Adjusts acceleration limits based on current speed + + + + Predictive: Uses future trajectory data to anticipate needed adjustments + + + + Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. + + + InputDialog @@ -538,13 +601,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - Installer - - Installing... - インストール中... - - LaneChangeSettings @@ -578,6 +634,22 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). + + + + Start the vehicle to check vehicle compatibility. + + + + This platform supports all MADS settings. + + + + This platform supports limited MADS settings. + + MadsSettings @@ -605,10 +677,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remain Active - - Pause Steering - - Disengage @@ -618,12 +686,179 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - -Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused when the brake pedal is manually pressed. + Start the vehicle to check vehicle compatibility. + + This feature defaults to OFF, and does not allow selection due to vehicle limitations. + + + + This feature defaults to ON, and does not allow selection due to vehicle limitations. + + + + This platform only supports Disengage mode due to vehicle limitations. + + + + Remain Active: ALC will remain active when the brake pedal is pressed. + + + + Pause + + + + Pause: ALC will pause when the brake pedal is pressed. + + + + Disengage: ALC will disengage when the brake pedal is pressed. + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + + + + + MaxTimeOffroad + + Max Time Offroad + + + + Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) + + + + Always On + + + + h + + + + m + + + + (default) + + + + + ModelsPanel + + Current Model + + + + SELECT + 選択 + + + No custom model selected! + + + + Driving + + + + Navigation + + + + Vision + + + + Policy + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] %3 + + + + downloaded + + + + ready + + + + from cache + + + + %1 model [%2] download failed + + + + %1 model [%2] pending... + + + + Fetching models... + + + + Use Default + + + + Select a Model + + + + Default + + + + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + キャリブレーションリセット + + + Driving Model Selector + + + + Warning: You are on a metered connection! + + + + Continue + 続ける + + + on Metered + + + + Cancel + キャンセル + MultiOptionDialog @@ -915,6 +1150,30 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed.Select a vehicle + + Unrecognized Vehicle + + + + Fingerprinted automatically + + + + Manually selected + + + + Not fingerprinted or manually selected + + + + Select vehicle to force fingerprint manually. + + + + Colors represent fingerprint status: + + PrimeAdWidget @@ -960,14 +1219,6 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed. QObject - - Reboot - 再起動 - - - Exit - 閉じる - sunnypilot sunnypilot @@ -1113,6 +1364,18 @@ This may take up to a minute. Steering + + Models + + + + Cruise + + + + Visuals + + Setup @@ -1404,108 +1667,20 @@ This may take up to a minute. SoftwarePanelSP - Current Model + Search Branch - SELECT - 選択 - - - No custom model selected! + Enter search keywords, or leave blank to list all branches. - Driving + No branches found for keywords: %1 - Navigation - - - - Metadata - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - キャリブレーションリセット - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 続ける - - - on Metered - - - - Cancel - キャンセル + Select a branch + ブランチを選択 @@ -1809,10 +1984,6 @@ This may take up to a minute. End-to-End Longitudinal Control End-to-Endアクセル制御 - - sunnypilot longitudinal control may come in a future update. - sunnypilotのアクセル制御は将来のアップデートで利用できる可能性があります。 - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. sunnypilotのアルファ版アクセル制御は、Experimentalモードと共に非リリースのブランチでテストすることができます。 @@ -1850,8 +2021,16 @@ This may take up to a minute. sunnypilot を有効化 - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - sunnypilotによるアダプティブクルーズコントロールとレーンキープアシストを利用します。この機能を利用する際は常に前方への注意が必要です。この設定を変更は車の電源が必要です。 + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + + + + Changing this setting will restart openpilot if the car is powered on. + + + + openpilot longitudinal control may come in a future update. + diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 34be6e5a2e..89f30ea545 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -62,10 +62,6 @@ Cellular Metered 데이터 요금제 - - Prevent large data uploads when on a metered connection - 데이터 요금제 연결 시 대용량 데이터 업로드를 방지합니다 - Hidden Network 숨겨진 네트워크 @@ -86,6 +82,30 @@ for "%1" "%1"에 접속하려면 비밀번호가 필요합니다 + + Prevent large data uploads when on a metered cellular connection + + + + default + + + + metered + + + + unmetered + + + + Wi-Fi Network Metered + + + + Prevent large data uploads when on a metered Wi-Fi connection + + AutoLaneChangeTimer @@ -151,18 +171,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode 롱컨 제어 모드 - - sunnypilot Longitudinal Control (Alpha) - sunnypilot 가감속 제어 (알파) - - - WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - 경고: sunnypilot 가감속 제어는 알파 기능으로 차량의 자동긴급제동(AEB)기능이 작동하지 않습니다. - - - On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. - 이 차량에서 sunnypilot은 sunnypilot 가감속 제어 대신 기본적으로 차량의 ACC로 가감속을 제어합니다. sunnypilot 가감속 제어로 전환하려면 이 기능을 활성화하세요. sunnypilot 가감속 제어 알파 기능을 활성화하는 경우 실험 모드 활성화를 권장합니다. - Enable ADB ADB 사용 @@ -171,14 +179,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB (안드로이드 디버그 브릿지) USB 또는 네트워크를 통해 장치에 연결할 수 있습니다. 자세한 내용은 https://docs.comma.ai/how-to/connect-to-comma를 참조하세요. - - Hyundai: Enable Radar Tracks - - - - Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. - - Enable GitHub runner service @@ -199,6 +199,18 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + DevicePanel @@ -342,6 +354,14 @@ Please use caution when using this feature. Only use the blinker when traffic an PAIR 동기화 + + Disengage to Reset Calibration + + + + Resetting calibration will restart openpilot if the car is powered on. + + DevicePanelSP @@ -525,6 +545,49 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX + + HyundaiSettings + + Off + + + + Dynamic + + + + Predictive + + + + Custom Longitudinal Tuning + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. + + + + Off: Uses default tuning + + + + Dynamic: Adjusts acceleration limits based on current speed + + + + Predictive: Uses future trajectory data to anticipate needed adjustments + + + + Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. + + + InputDialog @@ -538,13 +601,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - Installer - - Installing... - 설치 중... - - LaneChangeSettings @@ -578,6 +634,22 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). + + + + Start the vehicle to check vehicle compatibility. + + + + This platform supports all MADS settings. + + + + This platform supports limited MADS settings. + + MadsSettings @@ -605,10 +677,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remain Active - - Pause Steering - - Disengage @@ -618,12 +686,179 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - -Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused when the brake pedal is manually pressed. + Start the vehicle to check vehicle compatibility. + + This feature defaults to OFF, and does not allow selection due to vehicle limitations. + + + + This feature defaults to ON, and does not allow selection due to vehicle limitations. + + + + This platform only supports Disengage mode due to vehicle limitations. + + + + Remain Active: ALC will remain active when the brake pedal is pressed. + + + + Pause + + + + Pause: ALC will pause when the brake pedal is pressed. + + + + Disengage: ALC will disengage when the brake pedal is pressed. + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + + + + + MaxTimeOffroad + + Max Time Offroad + + + + Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) + + + + Always On + + + + h + + + + m + + + + (default) + + + + + ModelsPanel + + Current Model + + + + SELECT + 선택 + + + No custom model selected! + + + + Driving + + + + Navigation + + + + Vision + + + + Policy + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] %3 + + + + downloaded + + + + ready + + + + from cache + + + + %1 model [%2] download failed + + + + %1 model [%2] pending... + + + + Fetching models... + + + + Use Default + + + + Select a Model + + + + Default + + + + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + 캘리브레이션 + + + Driving Model Selector + + + + Warning: You are on a metered connection! + + + + Continue + 계속 + + + on Metered + + + + Cancel + 취소 + MultiOptionDialog @@ -915,6 +1150,30 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed.Select a vehicle + + Unrecognized Vehicle + + + + Fingerprinted automatically + + + + Manually selected + + + + Not fingerprinted or manually selected + + + + Select vehicle to force fingerprint manually. + + + + Colors represent fingerprint status: + + PrimeAdWidget @@ -960,14 +1219,6 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed. QObject - - Reboot - 재부팅 - - - Exit - 종료 - sunnypilot sunnypilot @@ -1113,6 +1364,18 @@ This may take up to a minute. Steering + + Models + + + + Cruise + + + + Visuals + + Setup @@ -1404,108 +1667,20 @@ This may take up to a minute. SoftwarePanelSP - Current Model + Search Branch - SELECT - 선택 - - - No custom model selected! + Enter search keywords, or leave blank to list all branches. - Driving + No branches found for keywords: %1 - Navigation - - - - Metadata - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - 캘리브레이션 - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 계속 - - - on Metered - - - - Cancel - 취소 + Select a branch + 브랜치 선택 @@ -1789,10 +1964,6 @@ This may take up to a minute. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. 차량에 장착된 ACC로 가감속을 제어하기 때문에 현재 이 차량에서는 실험 모드를 사용할 수 없습니다. - - sunnypilot longitudinal control may come in a future update. - sunnypilot 가감속 제어는 향후 업데이트에서 지원될 수 있습니다. - Aggressive 공격적 @@ -1850,8 +2021,16 @@ This may take up to a minute. sunnypilot 사용 - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - 어댑티브 크루즈 컨트롤 및 차로 유지 보조를 위해 sunnypilot 시스템을 사용할 수 있습니다. 이 기능을 사용할 때에는 언제나 주의를 기울여야 합니다. 설정을 변경하면 차량 시동이 꺼졌을 때 적용됩니다. + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + + + + Changing this setting will restart openpilot if the car is powered on. + + + + openpilot longitudinal control may come in a future update. + diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index bb37dba541..bc9e50ee0a 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -62,10 +62,6 @@ Cellular Metered Plano de Dados Limitado - - Prevent large data uploads when on a metered connection - Evite grandes uploads de dados quando estiver em uma conexão limitada - Hidden Network Rede Oculta @@ -86,6 +82,30 @@ for "%1" para "%1" + + Prevent large data uploads when on a metered cellular connection + + + + default + + + + metered + + + + unmetered + + + + Wi-Fi Network Metered + + + + Prevent large data uploads when on a metered Wi-Fi connection + + AutoLaneChangeTimer @@ -151,18 +171,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode Modo Longitudinal Maneuver - - sunnypilot Longitudinal Control (Alpha) - Controle Longitudinal sunnypilot (Embrionário) - - - WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - AVISO: o controle longitudinal sunnypilot está em estado embrionário para este carro e desativará a Frenagem Automática de Emergência (AEB). - - - On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. - Neste carro, o sunnypilot tem como padrão o ACC embutido do carro em vez do controle longitudinal do sunnypilot. Habilite isso para alternar para o controle longitudinal sunnypilot. Recomenda-se ativar o modo Experimental ao ativar o embrionário controle longitudinal sunnypilot. - Enable ADB Habilitar ADB @@ -171,14 +179,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB (Android Debug Bridge) permite conectar ao seu dispositivo por meio do USB ou através da rede. Veja https://docs.comma.ai/how-to/connect-to-comma para maiores informações. - - Hyundai: Enable Radar Tracks - - - - Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. - - Enable GitHub runner service @@ -199,6 +199,18 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + DevicePanel @@ -342,6 +354,14 @@ Please use caution when using this feature. Only use the blinker when traffic an PAIR PAREAR + + Disengage to Reset Calibration + + + + Resetting calibration will restart openpilot if the car is powered on. + + DevicePanelSP @@ -526,6 +546,49 @@ Firehose Mode allows you to maximize your training data uploads to improve openp LIMITE + + HyundaiSettings + + Off + + + + Dynamic + + + + Predictive + + + + Custom Longitudinal Tuning + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. + + + + Off: Uses default tuning + + + + Dynamic: Adjusts acceleration limits based on current speed + + + + Predictive: Uses future trajectory data to anticipate needed adjustments + + + + Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. + + + InputDialog @@ -540,13 +603,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - Installer - - Installing... - Instalando... - - LaneChangeSettings @@ -580,6 +636,22 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). + + + + Start the vehicle to check vehicle compatibility. + + + + This platform supports all MADS settings. + + + + This platform supports limited MADS settings. + + MadsSettings @@ -607,10 +679,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remain Active - - Pause Steering - - Disengage @@ -620,12 +688,179 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - -Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused when the brake pedal is manually pressed. + Start the vehicle to check vehicle compatibility. + + This feature defaults to OFF, and does not allow selection due to vehicle limitations. + + + + This feature defaults to ON, and does not allow selection due to vehicle limitations. + + + + This platform only supports Disengage mode due to vehicle limitations. + + + + Remain Active: ALC will remain active when the brake pedal is pressed. + + + + Pause + + + + Pause: ALC will pause when the brake pedal is pressed. + + + + Disengage: ALC will disengage when the brake pedal is pressed. + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + + + + + MaxTimeOffroad + + Max Time Offroad + + + + Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) + + + + Always On + + + + h + + + + m + + + + (default) + + + + + ModelsPanel + + Current Model + + + + SELECT + SELECIONE + + + No custom model selected! + + + + Driving + + + + Navigation + + + + Vision + + + + Policy + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] %3 + + + + downloaded + + + + ready + + + + from cache + + + + %1 model [%2] download failed + + + + %1 model [%2] pending... + + + + Fetching models... + + + + Use Default + + + + Select a Model + + + + Default + + + + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + Reinicializar Calibragem + + + Driving Model Selector + + + + Warning: You are on a metered connection! + + + + Continue + Continuar + + + on Metered + + + + Cancel + Cancelar + MultiOptionDialog @@ -917,6 +1152,30 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed.Select a vehicle + + Unrecognized Vehicle + + + + Fingerprinted automatically + + + + Manually selected + + + + Not fingerprinted or manually selected + + + + Select vehicle to force fingerprint manually. + + + + Colors represent fingerprint status: + + PrimeAdWidget @@ -962,14 +1221,6 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed. QObject - - Reboot - Reiniciar - - - Exit - Sair - sunnypilot sunnypilot @@ -1118,6 +1369,18 @@ Isso pode levar até um minuto. Steering + + Models + + + + Cruise + + + + Visuals + + Setup @@ -1409,108 +1672,20 @@ Isso pode levar até um minuto. SoftwarePanelSP - Current Model + Search Branch - SELECT - SELECIONE - - - No custom model selected! + Enter search keywords, or leave blank to list all branches. - Driving + No branches found for keywords: %1 - Navigation - - - - Metadata - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Reinicializar Calibragem - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Continuar - - - on Metered - - - - Cancel - Cancelar + Select a branch + Selecione uma branch @@ -1794,10 +1969,6 @@ Isso pode levar até um minuto. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. O modo Experimental está atualmente indisponível para este carro já que o ACC original do carro é usado para controle longitudinal. - - sunnypilot longitudinal control may come in a future update. - O controle longitudinal sunnypilot poderá vir em uma atualização futura. - Aggressive Disputa @@ -1855,8 +2026,16 @@ Isso pode levar até um minuto. Ativar sunnypilot - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Use o sistema sunnypilot para controle de cruzeiro adaptativo e assistência ao motorista de manutenção de faixa. Sua atenção é necessária o tempo todo para usar esse recurso. A alteração desta configuração tem efeito quando o carro é desligado. + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + + + + Changing this setting will restart openpilot if the car is powered on. + + + + openpilot longitudinal control may come in a future update. + diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 765aff4ab0..2c3d0bdb6c 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -62,10 +62,6 @@ Cellular Metered ลดการส่งข้อมูลผ่านเซลลูล่าร์ - - Prevent large data uploads when on a metered connection - ปิดการอัพโหลดข้อมูลขนาดใหญ่เมื่อเชื่อมต่อผ่านเซลลูล่าร์ - Hidden Network เครือข่ายที่ซ่อนอยู่ @@ -86,6 +82,30 @@ for "%1" สำหรับ "%1" + + Prevent large data uploads when on a metered cellular connection + + + + default + + + + metered + + + + unmetered + + + + Wi-Fi Network Metered + + + + Prevent large data uploads when on a metered Wi-Fi connection + + AutoLaneChangeTimer @@ -151,18 +171,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode - - sunnypilot Longitudinal Control (Alpha) - ระบบควบคุมการเร่ง/เบรคโดย sunnypilot (Alpha) - - - WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - คำเตือน: การควบคุมการเร่ง/เบรคโดย sunnypilot สำหรับรถคันนี้ยังอยู่ในสถานะ alpha และระบบเบรคฉุกเฉินอัตโนมัติ (AEB) จะถูกปิด - - - On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. - โดยปกติสำหรับรถคันนี้ sunnypilot จะควบคุมการเร่ง/เบรคด้วยระบบ ACC จากโรงงาน แทนการควยคุมโดย sunnypilot เปิดสวิตซ์นี้เพื่อให้ sunnypilot ควบคุมการเร่ง/เบรค แนะนำให้เปิดโหมดทดลองเมื่อต้องการให้ sunnypilot ควบคุมการเร่ง/เบรค ซึ่งอยู่ในสถานะ alpha - Enable ADB @@ -171,14 +179,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - Hyundai: Enable Radar Tracks - - - - Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. - - Enable GitHub runner service @@ -199,6 +199,18 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + DevicePanel @@ -342,6 +354,14 @@ Please use caution when using this feature. Only use the blinker when traffic an PAIR จับคู่ + + Disengage to Reset Calibration + + + + Resetting calibration will restart openpilot if the car is powered on. + + DevicePanelSP @@ -525,6 +545,49 @@ Firehose Mode allows you to maximize your training data uploads to improve openp สูงสุด + + HyundaiSettings + + Off + + + + Dynamic + + + + Predictive + + + + Custom Longitudinal Tuning + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. + + + + Off: Uses default tuning + + + + Dynamic: Adjusts acceleration limits based on current speed + + + + Predictive: Uses future trajectory data to anticipate needed adjustments + + + + Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. + + + InputDialog @@ -538,13 +601,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - Installer - - Installing... - กำลังติดตั้ง... - - LaneChangeSettings @@ -578,6 +634,22 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). + + + + Start the vehicle to check vehicle compatibility. + + + + This platform supports all MADS settings. + + + + This platform supports limited MADS settings. + + MadsSettings @@ -605,10 +677,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remain Active - - Pause Steering - - Disengage @@ -618,12 +686,179 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - -Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused when the brake pedal is manually pressed. + Start the vehicle to check vehicle compatibility. + + This feature defaults to OFF, and does not allow selection due to vehicle limitations. + + + + This feature defaults to ON, and does not allow selection due to vehicle limitations. + + + + This platform only supports Disengage mode due to vehicle limitations. + + + + Remain Active: ALC will remain active when the brake pedal is pressed. + + + + Pause + + + + Pause: ALC will pause when the brake pedal is pressed. + + + + Disengage: ALC will disengage when the brake pedal is pressed. + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + + + + + MaxTimeOffroad + + Max Time Offroad + + + + Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) + + + + Always On + + + + h + + + + m + + + + (default) + + + + + ModelsPanel + + Current Model + + + + SELECT + เลือก + + + No custom model selected! + + + + Driving + + + + Navigation + + + + Vision + + + + Policy + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] %3 + + + + downloaded + + + + ready + + + + from cache + + + + %1 model [%2] download failed + + + + %1 model [%2] pending... + + + + Fetching models... + + + + Use Default + + + + Select a Model + + + + Default + + + + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + รีเซ็ตการคาลิเบรท + + + Driving Model Selector + + + + Warning: You are on a metered connection! + + + + Continue + ดำเนินการต่อ + + + on Metered + + + + Cancel + ยกเลิก + MultiOptionDialog @@ -915,6 +1150,30 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed.Select a vehicle + + Unrecognized Vehicle + + + + Fingerprinted automatically + + + + Manually selected + + + + Not fingerprinted or manually selected + + + + Select vehicle to force fingerprint manually. + + + + Colors represent fingerprint status: + + PrimeAdWidget @@ -960,14 +1219,6 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed. QObject - - Reboot - รีบูต - - - Exit - ปิด - sunnypilot sunnypilot @@ -1113,6 +1364,18 @@ This may take up to a minute. Steering + + Models + + + + Cruise + + + + Visuals + + Setup @@ -1404,108 +1667,20 @@ This may take up to a minute. SoftwarePanelSP - Current Model + Search Branch - SELECT - เลือก - - - No custom model selected! + Enter search keywords, or leave blank to list all branches. - Driving + No branches found for keywords: %1 - Navigation - - - - Metadata - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - รีเซ็ตการคาลิเบรท - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - ดำเนินการต่อ - - - on Metered - - - - Cancel - ยกเลิก + Select a branch + เลือก Branch @@ -1789,10 +1964,6 @@ This may take up to a minute. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. ขณะนี้โหมดทดลองไม่สามารถใช้งานได้ในรถคันนี้ เนื่องจากเปิดใช้ระบบควบคุมการเร่ง/เบรคของรถที่ติดตั้งจากโรงงานอยู่ - - sunnypilot longitudinal control may come in a future update. - ระบบควบคุมการเร่ง/เบรคโดย sunnypilot อาจมาในการอัปเดตในอนาคต - Aggressive ดุดัน @@ -1850,8 +2021,16 @@ This may take up to a minute. เปิดใช้งาน sunnypilot - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - ใช้ระบบ sunnypilot สำหรับระบบควบคุมความเร็วอัตโนมัติ และระบบช่วยควบคุมรถให้อยู่ในเลน คุณจำเป็นต้องให้ความสนใจตลอดเวลาที่ใช้คุณสมบัตินี้ การเปลี่ยนการตั้งค่านี้จะมีผลเมื่อคุณดับเครื่องยนต์ + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + + + + Changing this setting will restart openpilot if the car is powered on. + + + + openpilot longitudinal control may come in a future update. + diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index 1ddf267e4b..fd06e81ef1 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -62,10 +62,6 @@ Cellular Metered - - Prevent large data uploads when on a metered connection - - Hidden Network @@ -86,6 +82,30 @@ for "%1" için "%1" + + Prevent large data uploads when on a metered cellular connection + + + + default + + + + metered + + + + unmetered + + + + Wi-Fi Network Metered + + + + Prevent large data uploads when on a metered Wi-Fi connection + + AutoLaneChangeTimer @@ -151,18 +171,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode - - sunnypilot Longitudinal Control (Alpha) - - - - WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. - - Enable ADB @@ -171,14 +179,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - - Hyundai: Enable Radar Tracks - - - - Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. - - Enable GitHub runner service @@ -199,6 +199,18 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + DevicePanel @@ -342,6 +354,14 @@ Please use caution when using this feature. Only use the blinker when traffic an PAIR + + Disengage to Reset Calibration + + + + Resetting calibration will restart openpilot if the car is powered on. + + DevicePanelSP @@ -525,6 +545,49 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX + + HyundaiSettings + + Off + + + + Dynamic + + + + Predictive + + + + Custom Longitudinal Tuning + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. + + + + Off: Uses default tuning + + + + Dynamic: Adjusts acceleration limits based on current speed + + + + Predictive: Uses future trajectory data to anticipate needed adjustments + + + + Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. + + + InputDialog @@ -538,13 +601,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - Installer - - Installing... - Yükleniyor... - - LaneChangeSettings @@ -578,6 +634,22 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). + + + + Start the vehicle to check vehicle compatibility. + + + + This platform supports all MADS settings. + + + + This platform supports limited MADS settings. + + MadsSettings @@ -605,10 +677,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remain Active - - Pause Steering - - Disengage @@ -618,10 +686,177 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - -Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused when the brake pedal is manually pressed. + Start the vehicle to check vehicle compatibility. + + + + This feature defaults to OFF, and does not allow selection due to vehicle limitations. + + + + This feature defaults to ON, and does not allow selection due to vehicle limitations. + + + + This platform only supports Disengage mode due to vehicle limitations. + + + + Remain Active: ALC will remain active when the brake pedal is pressed. + + + + Pause + + + + Pause: ALC will pause when the brake pedal is pressed. + + + + Disengage: ALC will disengage when the brake pedal is pressed. + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + + + + + MaxTimeOffroad + + Max Time Offroad + + + + Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) + + + + Always On + + + + h + + + + m + + + + (default) + + + + + ModelsPanel + + Current Model + + + + SELECT + + + + No custom model selected! + + + + Driving + + + + Navigation + + + + Vision + + + + Policy + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] %3 + + + + downloaded + + + + ready + + + + from cache + + + + %1 model [%2] download failed + + + + %1 model [%2] pending... + + + + Fetching models... + + + + Use Default + + + + Select a Model + + + + Default + + + + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + Kalibrasyonu sıfırla + + + Driving Model Selector + + + + Warning: You are on a metered connection! + + + + Continue + Devam et + + + on Metered + + + + Cancel @@ -914,6 +1149,30 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed.Select a vehicle + + Unrecognized Vehicle + + + + Fingerprinted automatically + + + + Manually selected + + + + Not fingerprinted or manually selected + + + + Select vehicle to force fingerprint manually. + + + + Colors represent fingerprint status: + + PrimeAdWidget @@ -959,14 +1218,6 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed. QObject - - Reboot - Yeniden başlat - - - Exit - Çık - sunnypilot sunnypilot @@ -1111,6 +1362,18 @@ This may take up to a minute. Steering + + Models + + + + Cruise + + + + Visuals + + Setup @@ -1402,107 +1665,19 @@ This may take up to a minute. SoftwarePanelSP - Current Model + Search Branch - SELECT + Enter search keywords, or leave blank to list all branches. - No custom model selected! + No branches found for keywords: %1 - Driving - - - - Navigation - - - - Metadata - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - Kalibrasyonu sıfırla - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - Devam et - - - on Metered - - - - Cancel + Select a branch @@ -1807,10 +1982,6 @@ This may take up to a minute. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - - sunnypilot longitudinal control may come in a future update. - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. @@ -1848,8 +2019,16 @@ This may take up to a minute. sunnypilot'u aktifleştir - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - Ayarlanabilir hız sabitleyici ve şeritte kalma yardımı için sunnypilot sistemini kullanın. Bu özelliği kullanırken her zaman dikkatli olmanız gerekiyor. Bu ayarın değiştirilmesi için araç kapatılıp açılması gerekiyor. + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + + + + Changing this setting will restart openpilot if the car is powered on. + + + + openpilot longitudinal control may come in a future update. + diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index d336fef2c7..06c6c67152 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -62,10 +62,6 @@ Cellular Metered 按流量计费的手机移动网络 - - Prevent large data uploads when on a metered connection - 当使用按流量计费的连接时,避免上传大流量数据 - Hidden Network 隐藏的网络 @@ -86,6 +82,30 @@ for "%1" 网络名称:"%1" + + Prevent large data uploads when on a metered cellular connection + + + + default + + + + metered + + + + unmetered + + + + Wi-Fi Network Metered + + + + Prevent large data uploads when on a metered Wi-Fi connection + + AutoLaneChangeTimer @@ -151,18 +171,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode 纵向操控测试模式 - - sunnypilot Longitudinal Control (Alpha) - sunnypilot纵向控制(Alpha 版) - - - WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - 警告:此车辆的 sunnypilot 纵向控制功能目前处于Alpha版本,使用此功能将会停用自动紧急制动(AEB)功能。 - - - On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. - 在这辆车上,sunnypilot 默认使用车辆内建的主动巡航控制(ACC),而非 sunnypilot 的纵向控制。启用此项功能可切换至 sunnypilot 的纵向控制。当启用 sunnypilot 纵向控制 Alpha 版本时,建议同时启用实验性模式(Experimental mode)。 - Enable ADB 启用 ADB @@ -171,14 +179,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android调试桥接)允许通过USB或网络连接到您的设备。更多信息请参见 [https://docs.comma.ai/how-to/connect-to-comma](https://docs.comma.ai/how-to/connect-to-comma)。 - - Hyundai: Enable Radar Tracks - - - - Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. - - Enable GitHub runner service @@ -199,6 +199,18 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + DevicePanel @@ -342,6 +354,14 @@ Please use caution when using this feature. Only use the blinker when traffic an PAIR 配对 + + Disengage to Reset Calibration + + + + Resetting calibration will restart openpilot if the car is powered on. + + DevicePanelSP @@ -525,6 +545,49 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 最高定速 + + HyundaiSettings + + Off + + + + Dynamic + + + + Predictive + + + + Custom Longitudinal Tuning + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. + + + + Off: Uses default tuning + + + + Dynamic: Adjusts acceleration limits based on current speed + + + + Predictive: Uses future trajectory data to anticipate needed adjustments + + + + Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. + + + InputDialog @@ -538,13 +601,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - Installer - - Installing... - 正在安装…… - - LaneChangeSettings @@ -578,6 +634,22 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). + + + + Start the vehicle to check vehicle compatibility. + + + + This platform supports all MADS settings. + + + + This platform supports limited MADS settings. + + MadsSettings @@ -605,10 +677,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remain Active - - Pause Steering - - Disengage @@ -618,12 +686,179 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - -Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused when the brake pedal is manually pressed. + Start the vehicle to check vehicle compatibility. + + This feature defaults to OFF, and does not allow selection due to vehicle limitations. + + + + This feature defaults to ON, and does not allow selection due to vehicle limitations. + + + + This platform only supports Disengage mode due to vehicle limitations. + + + + Remain Active: ALC will remain active when the brake pedal is pressed. + + + + Pause + + + + Pause: ALC will pause when the brake pedal is pressed. + + + + Disengage: ALC will disengage when the brake pedal is pressed. + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + + + + + MaxTimeOffroad + + Max Time Offroad + + + + Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) + + + + Always On + + + + h + + + + m + + + + (default) + + + + + ModelsPanel + + Current Model + + + + SELECT + 选择 + + + No custom model selected! + + + + Driving + + + + Navigation + + + + Vision + + + + Policy + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] %3 + + + + downloaded + + + + ready + + + + from cache + + + + %1 model [%2] download failed + + + + %1 model [%2] pending... + + + + Fetching models... + + + + Use Default + + + + Select a Model + + + + Default + + + + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + 重置设备校准 + + + Driving Model Selector + + + + Warning: You are on a metered connection! + + + + Continue + 继续 + + + on Metered + + + + Cancel + 取消 + MultiOptionDialog @@ -915,6 +1150,30 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed.Select a vehicle + + Unrecognized Vehicle + + + + Fingerprinted automatically + + + + Manually selected + + + + Not fingerprinted or manually selected + + + + Select vehicle to force fingerprint manually. + + + + Colors represent fingerprint status: + + PrimeAdWidget @@ -960,14 +1219,6 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed. QObject - - Reboot - 重启 - - - Exit - 退出 - sunnypilot sunnypilot @@ -1113,6 +1364,18 @@ This may take up to a minute. Steering + + Models + + + + Cruise + + + + Visuals + + Setup @@ -1404,108 +1667,20 @@ This may take up to a minute. SoftwarePanelSP - Current Model + Search Branch - SELECT - 选择 - - - No custom model selected! + Enter search keywords, or leave blank to list all branches. - Driving + No branches found for keywords: %1 - Navigation - - - - Metadata - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - 重置设备校准 - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 继续 - - - on Metered - - - - Cancel - 取消 + Select a branch + 选择分支 @@ -1789,10 +1964,6 @@ This may take up to a minute. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. 由于此车辆使用自带的ACC纵向控制,当前无法使用试验模式。 - - sunnypilot longitudinal control may come in a future update. - sunnypilot纵向控制可能会在未来的更新中提供。 - Aggressive 积极 @@ -1850,8 +2021,16 @@ This may take up to a minute. 启用sunnypilot - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - 使用sunnypilot进行自适应巡航和车道保持辅助。使用此功能时您必须时刻保持注意力。该设置的更改在熄火时生效。 + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + + + + Changing this setting will restart openpilot if the car is powered on. + + + + openpilot longitudinal control may come in a future update. + diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index f69e106872..c46157e78e 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -62,10 +62,6 @@ Cellular Metered 行動網路 - - Prevent large data uploads when on a metered connection - 防止使用行動網路上傳大量的數據 - Hidden Network 隱藏的網路 @@ -86,6 +82,30 @@ for "%1" 給 "%1" + + Prevent large data uploads when on a metered cellular connection + + + + default + + + + metered + + + + unmetered + + + + Wi-Fi Network Metered + + + + Prevent large data uploads when on a metered Wi-Fi connection + + AutoLaneChangeTimer @@ -151,18 +171,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode 縱向操控測試模式 - - sunnypilot Longitudinal Control (Alpha) - sunnypilot 縱向控制 (Alpha 版) - - - WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - 警告:此車輛的 sunnypilot 縱向控制功能目前處於 Alpha 版本,使用此功能將會停用自動緊急煞車(AEB)功能。 - - - On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. - 在這輛車上,sunnypilot 預設使用車輛內建的主動巡航控制(ACC),而非 sunnypilot 的縱向控制。啟用此項功能可切換至 sunnypilot 的縱向控制。當啟用 sunnypilot 縱向控制 Alpha 版本時,建議同時啟用實驗性模式(Experimental mode)。 - Enable ADB 啟用 ADB @@ -171,14 +179,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android 調試橋接)允許通過 USB 或網絡連接到您的設備。更多信息請參見 [https://docs.comma.ai/how-to/connect-to-comma](https://docs.comma.ai/how-to/connect-to-comma)。 - - Hyundai: Enable Radar Tracks - - - - Enable this to attempt to enable radar tracks for Hyundai, Kia, and Genesis models equipped with the supported Mando SCC radar. This allows sunnypilot to use radar data for improved lead tracking and overall longitudinal performance. - - Enable GitHub runner service @@ -199,6 +199,18 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + DevicePanel @@ -342,6 +354,14 @@ Please use caution when using this feature. Only use the blinker when traffic an PAIR 配對 + + Disengage to Reset Calibration + + + + Resetting calibration will restart openpilot if the car is powered on. + + DevicePanelSP @@ -525,6 +545,49 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 最高 + + HyundaiSettings + + Off + + + + Dynamic + + + + Predictive + + + + Custom Longitudinal Tuning + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. + + + + Off: Uses default tuning + + + + Dynamic: Adjusts acceleration limits based on current speed + + + + Predictive: Uses future trajectory data to anticipate needed adjustments + + + + Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. + + + InputDialog @@ -538,13 +601,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - Installer - - Installing... - 安裝中… - - LaneChangeSettings @@ -578,6 +634,22 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). + + + + Start the vehicle to check vehicle compatibility. + + + + This platform supports all MADS settings. + + + + This platform supports limited MADS settings. + + MadsSettings @@ -605,10 +677,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remain Active - - Pause Steering - - Disengage @@ -618,12 +686,179 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - -Remain Active: ALC will remain active even after the brake pedal is pressed. -Pause Steering: ALC will be paused when the brake pedal is manually pressed. + Start the vehicle to check vehicle compatibility. + + This feature defaults to OFF, and does not allow selection due to vehicle limitations. + + + + This feature defaults to ON, and does not allow selection due to vehicle limitations. + + + + This platform only supports Disengage mode due to vehicle limitations. + + + + Remain Active: ALC will remain active when the brake pedal is pressed. + + + + Pause + + + + Pause: ALC will pause when the brake pedal is pressed. + + + + Disengage: ALC will disengage when the brake pedal is pressed. + + + + Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. + + + + + MaxTimeOffroad + + Max Time Offroad + + + + Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) + + + + Always On + + + + h + + + + m + + + + (default) + + + + + ModelsPanel + + Current Model + + + + SELECT + 選取 + + + No custom model selected! + + + + Driving + + + + Navigation + + + + Vision + + + + Policy + + + + Downloading %1 model [%2]... (%3%) + + + + %1 model [%2] %3 + + + + downloaded + + + + ready + + + + from cache + + + + %1 model [%2] download failed + + + + %1 model [%2] pending... + + + + Fetching models... + + + + Use Default + + + + Select a Model + + + + Default + + + + Model download has started in the background. + + + + We STRONGLY suggest you to reset calibration. + + + + Would you like to do that now? + + + + Reset Calibration + 重設校準 + + + Driving Model Selector + + + + Warning: You are on a metered connection! + + + + Continue + 繼續 + + + on Metered + + + + Cancel + 取消 + MultiOptionDialog @@ -915,6 +1150,30 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed.Select a vehicle + + Unrecognized Vehicle + + + + Fingerprinted automatically + + + + Manually selected + + + + Not fingerprinted or manually selected + + + + Select vehicle to force fingerprint manually. + + + + Colors represent fingerprint status: + + PrimeAdWidget @@ -960,14 +1219,6 @@ Pause Steering: ALC will be paused when the brake pedal is manually pressed. QObject - - Reboot - 重新啟動 - - - Exit - 離開 - sunnypilot sunnypilot @@ -1113,6 +1364,18 @@ This may take up to a minute. Steering + + Models + + + + Cruise + + + + Visuals + + Setup @@ -1404,108 +1667,20 @@ This may take up to a minute. SoftwarePanelSP - Current Model + Search Branch - SELECT - 選取 - - - No custom model selected! + Enter search keywords, or leave blank to list all branches. - Driving + No branches found for keywords: %1 - Navigation - - - - Metadata - - - - Downloading %1 model [%2]... (%3%) - - - - %1 model [%2] %3 - - - - downloaded - - - - ready - - - - from cache - - - - %1 model [%2] download failed - - - - %1 model [%2] pending... - - - - Fetching models... - - - - Use Default - - - - Select a Model - - - - Default - - - - Model download has started in the background. - - - - We STRONGLY suggest you to reset calibration. - - - - Would you like to do that now? - - - - Reset Calibration - 重設校準 - - - Driving Model Selector - - - - Warning: You are on a metered connection! - - - - Continue - 繼續 - - - on Metered - - - - Cancel - 取消 + Select a branch + 選取一個分支 @@ -1789,10 +1964,6 @@ This may take up to a minute. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. 因車輛使用內建ACC系統,無法在本車輛上啟動實驗模式。 - - sunnypilot longitudinal control may come in a future update. - sunnypilot 縱向控制可能會在未來的更新中提供。 - Aggressive 積極 @@ -1850,8 +2021,16 @@ This may take up to a minute. 啟用 sunnypilot - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - 使用 sunnypilot 的主動式巡航和車道保持功能,開啟後您需要持續集中注意力,設定變更在重新啟動車輛後生效。 + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + + + + Changing this setting will restart openpilot if the car is powered on. + + + + openpilot longitudinal control may come in a future update. + diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 3b565e0e13..c9dc70cdff 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -80,8 +80,12 @@ void UIState::updateStatus() { status = STATUS_OVERRIDE; } else { if (mads.getAvailable()) { - if (mads.getEnabled()) { - status = ss.getEnabled() ? STATUS_ENGAGED : STATUS_LAT_ONLY; + if (mads.getEnabled() && ss.getEnabled()) { + status = STATUS_ENGAGED; + } else if (mads.getEnabled()) { + status = STATUS_LAT_ONLY; + } else if (ss.getEnabled()) { + status = STATUS_LONG_ONLY; } else { status = STATUS_DISENGAGED; } @@ -91,6 +95,11 @@ void UIState::updateStatus() { } } + if (engaged() != engaged_prev) { + engaged_prev = engaged(); + emit engagedChanged(engaged()); + } + // Handle onroad/offroad transition if (scene.started != started_prev || sm->frame == 1) { if (scene.started) { @@ -162,6 +171,8 @@ void Device::resetInteractiveTimeout(int timeout) { } void Device::updateBrightness(const UIState &s) { + int brightness; + int brightness_override = QString::fromStdString(Params().get("Brightness")).toInt(); float clipped_brightness = offroad_brightness; if (s.scene.started && s.scene.light_sensor >= 0) { clipped_brightness = s.scene.light_sensor; @@ -173,11 +184,19 @@ void Device::updateBrightness(const UIState &s) { clipped_brightness = std::pow((clipped_brightness + 16.0) / 116.0, 3.0); } - // Scale back to 10% to 100% - clipped_brightness = std::clamp(100.0f * clipped_brightness, 10.0f, 100.0f); + if (brightness_override == 1) { + clipped_brightness = std::clamp(100.0f * clipped_brightness, 1.0f, 100.0f); // Scale back to 1% to 100% + } else if (brightness_override == 0) { + clipped_brightness = std::clamp(100.0f * clipped_brightness, 10.0f, 100.0f); // Scale back to 10% to 100% + } + } + + if (brightness_override == 0 || brightness_override == 1) { + brightness = brightness_filter.update(clipped_brightness); + } else { + brightness = brightness_override; } - int brightness = brightness_filter.update(clipped_brightness); if (!awake) { brightness = 0; } diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 21e74af1d2..4ead8aecb9 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -72,7 +72,7 @@ class UIState : public QObject { public: UIState(QObject* parent = 0); virtual void updateStatus(); - inline bool engaged() const { + virtual inline bool engaged() const { return scene.started && (*sm)["selfdriveState"].getSelfdriveState().getEnabled(); } @@ -85,6 +85,7 @@ public: signals: void uiUpdate(const UIState &s); void offroadTransition(bool offroad); + void engagedChanged(bool engaged); protected slots: virtual void update(); @@ -94,6 +95,7 @@ protected: private: bool started_prev = false; + bool engaged_prev = false; }; #ifndef SUNNYPILOT diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py new file mode 100755 index 0000000000..bb2b431e03 --- /dev/null +++ b/selfdrive/ui/ui.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import pyray as rl +from openpilot.common.watchdog import kick_watchdog +from openpilot.system.ui.lib.application import gui_app +from openpilot.selfdrive.ui.layouts.main import MainLayout +from openpilot.selfdrive.ui.ui_state import ui_state + + +def main(): + gui_app.init_window("UI") + main_layout = MainLayout() + main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + for _ in gui_app.render(): + ui_state.update() + + # TODO handle brigntness and awake state here + + main_layout.render() + + kick_watchdog() + + +if __name__ == "__main__": + main() diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py new file mode 100644 index 0000000000..3738ea6d5d --- /dev/null +++ b/selfdrive/ui/ui_state.py @@ -0,0 +1,171 @@ +import pyray as rl +import time +from collections.abc import Callable +from enum import Enum +from cereal import messaging, log +from openpilot.common.params import Params, UnknownKeyName +from openpilot.selfdrive.ui.lib.prime_state import PrimeState + +UI_BORDER_SIZE = 30 + + +class UIStatus(Enum): + DISENGAGED = "disengaged" + ENGAGED = "engaged" + OVERRIDE = "override" + + +class UIState: + _instance: 'UIState | None' = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialize() + return cls._instance + + def _initialize(self): + self.params = Params() + self.sm = messaging.SubMaster( + [ + "modelV2", + "controlsState", + "liveCalibration", + "radarState", + "deviceState", + "pandaStates", + "carParams", + "driverMonitoringState", + "carState", + "driverStateV2", + "roadCameraState", + "wideRoadCameraState", + "managerState", + "selfdriveState", + "longitudinalPlan", + ] + ) + + self.prime_state = PrimeState() + + # UI Status tracking + self.status: UIStatus = UIStatus.DISENGAGED + self.started_frame: int = 0 + self._engaged_prev: bool = False + self._started_prev: bool = False + + # Core state variables + self.is_metric: bool = self.params.get_bool("IsMetric") + self.started: bool = False + self.ignition: bool = False + self.panda_type: log.PandaState.PandaType = log.PandaState.PandaType.unknown + self.personality: log.LongitudinalPersonality = log.LongitudinalPersonality.standard + self.light_sensor: float = -1.0 + + self._update_params() + + @property + def engaged(self) -> bool: + return self.started and self.sm["selfdriveState"].enabled + + def is_onroad(self) -> bool: + return self.started + + def is_offroad(self) -> bool: + return not self.started + + def update(self) -> None: + self.sm.update(0) + self._update_state() + self._update_status() + device.update() + + def _update_state(self) -> None: + # Handle panda states updates + if self.sm.updated["pandaStates"]: + panda_states = self.sm["pandaStates"] + + if len(panda_states) > 0: + # Get panda type from first panda + self.panda_type = panda_states[0].pandaType + # Check ignition status across all pandas + if self.panda_type != log.PandaState.PandaType.unknown: + self.ignition = any(state.ignitionLine or state.ignitionCan for state in panda_states) + elif self.sm.frame - self.sm.recv_frame["pandaStates"] > 5 * rl.get_fps(): + self.panda_type = log.PandaState.PandaType.unknown + + # Handle wide road camera state updates + if self.sm.updated["wideRoadCameraState"]: + cam_state = self.sm["wideRoadCameraState"] + + # Scale factor based on sensor type + scale = 6.0 if cam_state.sensor == 'ar0231' else 1.0 + self.light_sensor = max(100.0 - scale * cam_state.exposureValPercent, 0.0) + elif not self.sm.alive["wideRoadCameraState"] or not self.sm.valid["wideRoadCameraState"]: + self.light_sensor = -1 + + # Update started state + self.started = self.sm["deviceState"].started and self.ignition + + def _update_status(self) -> None: + if self.started and self.sm.updated["selfdriveState"]: + ss = self.sm["selfdriveState"] + state = ss.state + + if state in (log.SelfdriveState.OpenpilotState.preEnabled, log.SelfdriveState.OpenpilotState.overriding): + self.status = UIStatus.OVERRIDE + else: + self.status = UIStatus.ENGAGED if ss.enabled else UIStatus.DISENGAGED + + # Check for engagement state changes + if self.engaged != self._engaged_prev: + self._engaged_prev = self.engaged + + # Handle onroad/offroad transition + if self.started != self._started_prev or self.sm.frame == 1: + if self.started: + self.status = UIStatus.DISENGAGED + self.started_frame = self.sm.frame + + self._started_prev = self.started + + def _update_params(self) -> None: + try: + self.is_metric = self.params.get_bool("IsMetric") + except UnknownKeyName: + self.is_metric = False + + +class Device: + def __init__(self): + self._ignition = False + self._interaction_time: float = 0.0 + self._interactive_timeout_callbacks: list[Callable] = [] + self._prev_timed_out = False + self.reset_interactive_timeout() + + def reset_interactive_timeout(self, timeout: int = -1) -> None: + if timeout == -1: + timeout = 10 if ui_state.ignition else 30 + self._interaction_time = time.monotonic() + timeout + + def add_interactive_timeout_callback(self, callback: Callable): + self._interactive_timeout_callbacks.append(callback) + + def update(self): + # Handle interactive timeout + ignition_just_turned_off = not ui_state.ignition and self._ignition + self._ignition = ui_state.ignition + + interaction_timeout = time.monotonic() > self._interaction_time + if ignition_just_turned_off or rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT): + self.reset_interactive_timeout() + elif interaction_timeout and not self._prev_timed_out: + for callback in self._interactive_timeout_callbacks: + callback() + self._prev_timed_out = interaction_timeout + + +# Global instance +ui_state = UIState() +device = Device() diff --git a/selfdrive/ui/widgets/__init__.py b/selfdrive/ui/widgets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/selfdrive/ui/widgets/exp_mode_button.py b/selfdrive/ui/widgets/exp_mode_button.py new file mode 100644 index 0000000000..83caf413d1 --- /dev/null +++ b/selfdrive/ui/widgets/exp_mode_button.py @@ -0,0 +1,74 @@ +import pyray as rl +from openpilot.common.params import Params +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.widget import Widget + + +class ExperimentalModeButton(Widget): + def __init__(self): + super().__init__() + + self.img_width = 80 + self.horizontal_padding = 50 + self.button_height = 125 + + 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) + + def _get_gradient_colors(self): + alpha = 0xCC if self.is_pressed else 0xFF + + if self.experimental_mode: + return rl.Color(255, 155, 63, alpha), rl.Color(219, 56, 34, alpha) + else: + return rl.Color(20, 255, 171, alpha), rl.Color(35, 149, 255, alpha) + + def _draw_gradient_background(self, rect): + start_color, end_color = self._get_gradient_colors() + 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 _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)) + self._draw_gradient_background(rect) + rl.end_scissor_mode() + + # Draw vertical separator line + line_x = rect.x + rect.width - self.img_width - (2 * self.horizontal_padding) + separator_color = rl.Color(0, 0, 0, 77) # 0x4d = 77 + rl.draw_line_ex(rl.Vector2(line_x, rect.y), rl.Vector2(line_x, rect.y + rect.height), 3, separator_color) + + # Draw text label (left aligned) + text = "EXPERIMENTAL MODE ON" if self.experimental_mode else "CHILL MODE ON" + text_x = rect.x + self.horizontal_padding + text_y = rect.y + rect.height / 2 - 45 // 2 # Center vertically + + rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.Color(0, 0, 0, 255)) + + # Draw icon (right aligned) + icon_x = rect.x + rect.width - self.horizontal_padding - self.img_width + icon_y = rect.y + (rect.height - self.img_width) / 2 + icon_rect = rl.Rectangle(icon_x, icon_y, self.img_width, self.img_width) + + # Draw current mode icon + current_icon = self.experimental_pixmap if self.experimental_mode else self.chill_pixmap + source_rect = rl.Rectangle(0, 0, current_icon.width, current_icon.height) + rl.draw_texture_pro(current_icon, source_rect, icon_rect, rl.Vector2(0, 0), 0, rl.Color(255, 255, 255, 255)) diff --git a/selfdrive/ui/widgets/offroad_alerts.py b/selfdrive/ui/widgets/offroad_alerts.py new file mode 100644 index 0000000000..aba07139db --- /dev/null +++ b/selfdrive/ui/widgets/offroad_alerts.py @@ -0,0 +1,330 @@ +import json +import pyray as rl +from abc import ABC, abstractmethod +from collections.abc import Callable +from dataclasses import dataclass +from openpilot.common.params import Params +from openpilot.system.hardware import HARDWARE +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.widget import Widget + + +class AlertColors: + HIGH_SEVERITY = rl.Color(226, 44, 44, 255) + LOW_SEVERITY = rl.Color(41, 41, 41, 255) + BACKGROUND = rl.Color(57, 57, 57, 255) + BUTTON = rl.WHITE + BUTTON_TEXT = rl.BLACK + SNOOZE_BG = rl.Color(79, 79, 79, 255) + TEXT = rl.WHITE + + +class AlertConstants: + BUTTON_SIZE = (400, 125) + SNOOZE_BUTTON_SIZE = (550, 125) + REBOOT_BUTTON_SIZE = (600, 125) + MARGIN = 50 + SPACING = 30 + FONT_SIZE = 48 + BORDER_RADIUS = 30 + ALERT_HEIGHT = 120 + ALERT_SPACING = 20 + + +@dataclass +class AlertData: + key: str + text: str + severity: int + visible: bool = False + + +class AbstractAlert(Widget, ABC): + def __init__(self, has_reboot_btn: bool = False): + super().__init__() + self.params = Params() + self.has_reboot_btn = has_reboot_btn + self.dismiss_callback: Callable | None = None + + self.dismiss_btn_rect = rl.Rectangle(0, 0, *AlertConstants.BUTTON_SIZE) + self.snooze_btn_rect = rl.Rectangle(0, 0, *AlertConstants.SNOOZE_BUTTON_SIZE) + self.reboot_btn_rect = rl.Rectangle(0, 0, *AlertConstants.REBOOT_BUTTON_SIZE) + + self.snooze_visible = False + self.content_rect = rl.Rectangle(0, 0, 0, 0) + self.scroll_panel_rect = rl.Rectangle(0, 0, 0, 0) + self.scroll_panel = GuiScrollPanel() + + def set_dismiss_callback(self, callback: Callable): + self.dismiss_callback = callback + + @abstractmethod + def refresh(self) -> bool: + pass + + @abstractmethod + def get_content_height(self) -> float: + pass + + def handle_input(self, mouse_pos: rl.Vector2, mouse_clicked: bool) -> bool: + if not mouse_clicked or not self.scroll_panel.is_touch_valid(): + return False + + if rl.check_collision_point_rec(mouse_pos, self.dismiss_btn_rect): + if self.dismiss_callback: + self.dismiss_callback() + return True + + if self.snooze_visible and rl.check_collision_point_rec(mouse_pos, self.snooze_btn_rect): + self.params.put_bool("SnoozeUpdate", True) + if self.dismiss_callback: + self.dismiss_callback() + return True + + if self.has_reboot_btn and rl.check_collision_point_rec(mouse_pos, self.reboot_btn_rect): + HARDWARE.reboot() + return True + + return False + + def _render(self, rect: rl.Rectangle): + rl.draw_rectangle_rounded(rect, AlertConstants.BORDER_RADIUS / rect.width, 10, AlertColors.BACKGROUND) + + footer_height = AlertConstants.BUTTON_SIZE[1] + AlertConstants.SPACING + content_height = rect.height - 2 * AlertConstants.MARGIN - footer_height + + self.content_rect = rl.Rectangle( + rect.x + AlertConstants.MARGIN, + rect.y + AlertConstants.MARGIN, + rect.width - 2 * AlertConstants.MARGIN, + content_height, + ) + self.scroll_panel_rect = rl.Rectangle( + self.content_rect.x, self.content_rect.y, self.content_rect.width, self.content_rect.height + ) + + self._render_scrollable_content() + self._render_footer(rect) + + def _render_scrollable_content(self): + content_total_height = self.get_content_height() + content_bounds = rl.Rectangle(0, 0, self.scroll_panel_rect.width, content_total_height) + scroll_offset = self.scroll_panel.handle_scroll(self.scroll_panel_rect, content_bounds) + + rl.begin_scissor_mode( + int(self.scroll_panel_rect.x), + int(self.scroll_panel_rect.y), + int(self.scroll_panel_rect.width), + int(self.scroll_panel_rect.height), + ) + + content_rect_with_scroll = rl.Rectangle( + self.scroll_panel_rect.x, + self.scroll_panel_rect.y + scroll_offset.y, + self.scroll_panel_rect.width, + content_total_height, + ) + + self._render_content(content_rect_with_scroll) + rl.end_scissor_mode() + + @abstractmethod + def _render_content(self, content_rect: rl.Rectangle): + pass + + def _render_footer(self, rect: rl.Rectangle): + footer_y = rect.y + rect.height - AlertConstants.MARGIN - AlertConstants.BUTTON_SIZE[1] + font = gui_app.font(FontWeight.MEDIUM) + + self.dismiss_btn_rect.x = rect.x + AlertConstants.MARGIN + self.dismiss_btn_rect.y = footer_y + rl.draw_rectangle_rounded(self.dismiss_btn_rect, 0.3, 10, AlertColors.BUTTON) + + text = "Close" + text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x + text_x = self.dismiss_btn_rect.x + (AlertConstants.BUTTON_SIZE[0] - text_width) // 2 + text_y = self.dismiss_btn_rect.y + (AlertConstants.BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2 + rl.draw_text_ex( + font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.BUTTON_TEXT + ) + + if self.snooze_visible: + self.snooze_btn_rect.x = rect.x + rect.width - AlertConstants.MARGIN - AlertConstants.SNOOZE_BUTTON_SIZE[0] + self.snooze_btn_rect.y = footer_y + rl.draw_rectangle_rounded(self.snooze_btn_rect, 0.3, 10, AlertColors.SNOOZE_BG) + + text = "Snooze Update" + text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x + text_x = self.snooze_btn_rect.x + (AlertConstants.SNOOZE_BUTTON_SIZE[0] - text_width) // 2 + text_y = self.snooze_btn_rect.y + (AlertConstants.SNOOZE_BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2 + rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.TEXT) + + elif self.has_reboot_btn: + self.reboot_btn_rect.x = rect.x + rect.width - AlertConstants.MARGIN - AlertConstants.REBOOT_BUTTON_SIZE[0] + self.reboot_btn_rect.y = footer_y + rl.draw_rectangle_rounded(self.reboot_btn_rect, 0.3, 10, AlertColors.BUTTON) + + text = "Reboot and Update" + text_width = measure_text_cached(font, text, AlertConstants.FONT_SIZE).x + text_x = self.reboot_btn_rect.x + (AlertConstants.REBOOT_BUTTON_SIZE[0] - text_width) // 2 + text_y = self.reboot_btn_rect.y + (AlertConstants.REBOOT_BUTTON_SIZE[1] - AlertConstants.FONT_SIZE) // 2 + rl.draw_text_ex( + font, text, rl.Vector2(int(text_x), int(text_y)), AlertConstants.FONT_SIZE, 0, AlertColors.BUTTON_TEXT + ) + + +class OffroadAlert(AbstractAlert): + def __init__(self): + super().__init__(has_reboot_btn=False) + self.sorted_alerts: list[AlertData] = [] + + def refresh(self): + if not self.sorted_alerts: + self._build_alerts() + + active_count = 0 + connectivity_needed = False + + for alert_data in self.sorted_alerts: + text = "" + bytes_data = self.params.get(alert_data.key) + + if bytes_data: + try: + alert_json = json.loads(bytes_data) + text = alert_json.get("text", "").replace("{}", alert_json.get("extra", "")) + except json.JSONDecodeError: + text = "" + + alert_data.text = text + alert_data.visible = bool(text) + + if alert_data.visible: + active_count += 1 + + if alert_data.key == "Offroad_ConnectivityNeeded" and alert_data.visible: + connectivity_needed = True + + self.snooze_visible = connectivity_needed + return active_count + + def get_content_height(self) -> float: + if not self.sorted_alerts: + return 0 + + total_height = 20 + font = gui_app.font(FontWeight.NORMAL) + + for alert_data in self.sorted_alerts: + if not alert_data.visible: + continue + + text_width = int(self.content_rect.width - 90) + wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width) + line_count = len(wrapped_lines) + text_height = line_count * (AlertConstants.FONT_SIZE + 5) + alert_item_height = max(text_height + 40, AlertConstants.ALERT_HEIGHT) + total_height += alert_item_height + AlertConstants.ALERT_SPACING + + if total_height > 20: + total_height = total_height - AlertConstants.ALERT_SPACING + 20 + + return total_height + + def _build_alerts(self): + self.sorted_alerts = [] + try: + with open("../selfdrived/alerts_offroad.json", "rb") as f: + alerts_config = json.load(f) + for key, config in sorted(alerts_config.items(), key=lambda x: x[1].get("severity", 0), reverse=True): + severity = config.get("severity", 0) + alert_data = AlertData(key=key, text="", severity=severity) + self.sorted_alerts.append(alert_data) + except (FileNotFoundError, json.JSONDecodeError): + pass + + def _render_content(self, content_rect: rl.Rectangle): + y_offset = 20 + font = gui_app.font(FontWeight.NORMAL) + + for alert_data in self.sorted_alerts: + if not alert_data.visible: + continue + + bg_color = AlertColors.HIGH_SEVERITY if alert_data.severity > 0 else AlertColors.LOW_SEVERITY + text_width = int(content_rect.width - 90) + wrapped_lines = wrap_text(font, alert_data.text, AlertConstants.FONT_SIZE, text_width) + line_count = len(wrapped_lines) + text_height = line_count * (AlertConstants.FONT_SIZE + 5) + alert_item_height = max(text_height + 40, AlertConstants.ALERT_HEIGHT) + + alert_rect = rl.Rectangle( + content_rect.x + 10, + content_rect.y + y_offset, + content_rect.width - 30, + alert_item_height, + ) + + rl.draw_rectangle_rounded(alert_rect, 0.2, 10, bg_color) + + text_x = alert_rect.x + 30 + text_y = alert_rect.y + 20 + + for i, line in enumerate(wrapped_lines): + rl.draw_text_ex( + font, + line, + rl.Vector2(text_x, text_y + i * (AlertConstants.FONT_SIZE + 5)), + AlertConstants.FONT_SIZE, + 0, + AlertColors.TEXT, + ) + + y_offset += alert_item_height + AlertConstants.ALERT_SPACING + + +class UpdateAlert(AbstractAlert): + def __init__(self): + super().__init__(has_reboot_btn=True) + self.release_notes = "" + self._wrapped_release_notes = "" + self._cached_content_height: float = 0.0 + + def refresh(self) -> bool: + update_available: bool = self.params.get_bool("UpdateAvailable") + if update_available: + self.release_notes = self.params.get("UpdaterNewReleaseNotes", encoding='utf-8') + self._cached_content_height = 0 + + return update_available + + def get_content_height(self) -> float: + if not self.release_notes: + return 100 + + if self._cached_content_height == 0: + self._wrapped_release_notes = self.release_notes + size = measure_text_cached(gui_app.font(FontWeight.NORMAL), self._wrapped_release_notes, AlertConstants.FONT_SIZE) + self._cached_content_height = max(size.y + 60, 100) + + return self._cached_content_height + + def _render_content(self, content_rect: rl.Rectangle): + if self.release_notes: + rl.draw_text_ex( + gui_app.font(FontWeight.NORMAL), + self._wrapped_release_notes, + rl.Vector2(content_rect.x + 30, content_rect.y + 30), + AlertConstants.FONT_SIZE, + 0.0, + AlertColors.TEXT, + ) + else: + no_notes_text = "No release notes available." + text_width = rl.measure_text(no_notes_text, AlertConstants.FONT_SIZE) + text_x = content_rect.x + (content_rect.width - text_width) // 2 + text_y = content_rect.y + 50 + rl.draw_text(no_notes_text, int(text_x), int(text_y), AlertConstants.FONT_SIZE, AlertColors.TEXT) diff --git a/selfdrive/ui/widgets/pairing_dialog.py b/selfdrive/ui/widgets/pairing_dialog.py new file mode 100644 index 0000000000..2cb1499499 --- /dev/null +++ b/selfdrive/ui/widgets/pairing_dialog.py @@ -0,0 +1,170 @@ +import pyray as rl +import qrcode +import numpy as np +import time + +from openpilot.common.api import Api +from openpilot.common.swaglog import cloudlog +from openpilot.common.params import Params +from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.lib.text_measure import measure_text_cached + + +class PairingDialog: + """Dialog for device pairing with QR code.""" + + QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds + + def __init__(self): + self.params = Params() + self.qr_texture: rl.Texture | None = None + self.last_qr_generation = 0 + + def _get_pairing_url(self) -> str: + try: + dongle_id = self.params.get("DongleId", encoding='utf8') or "" + token = Api(dongle_id).get_token() + except Exception as e: + cloudlog.warning(f"Failed to get pairing token: {e}") + token = "" + return f"https://connect.comma.ai/setup?token={token}" + + def _generate_qr_code(self) -> None: + try: + qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4) + qr.add_data(self._get_pairing_url()) + qr.make(fit=True) + + pil_img = qr.make_image(fill_color="black", back_color="white").convert('RGBA') + img_array = np.array(pil_img, dtype=np.uint8) + + if self.qr_texture and self.qr_texture.id != 0: + rl.unload_texture(self.qr_texture) + + rl_image = rl.Image() + rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data) + rl_image.width = pil_img.width + rl_image.height = pil_img.height + rl_image.mipmaps = 1 + rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 + + self.qr_texture = rl.load_texture_from_image(rl_image) + except Exception as e: + cloudlog.warning(f"QR code generation failed: {e}") + self.qr_texture = None + + def _check_qr_refresh(self) -> None: + current_time = time.time() + if current_time - self.last_qr_generation >= self.QR_REFRESH_INTERVAL: + self._generate_qr_code() + self.last_qr_generation = current_time + + def render(self, rect: rl.Rectangle) -> int: + rl.clear_background(rl.Color(224, 224, 224, 255)) + + self._check_qr_refresh() + + margin = 70 + content_rect = rl.Rectangle(rect.x + margin, rect.y + margin, rect.width - 2 * margin, rect.height - 2 * margin) + y = content_rect.y + + # Close button + close_size = 80 + close_icon = gui_app.texture("icons/close.png", close_size, close_size) + close_rect = rl.Rectangle(content_rect.x, y, close_size, close_size) + + mouse_pos = rl.get_mouse_position() + is_hover = rl.check_collision_point_rec(mouse_pos, close_rect) + is_pressed = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) + is_released = rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) + + color = rl.Color(180, 180, 180, 150) if (is_hover and is_pressed) else rl.WHITE + rl.draw_texture(close_icon, int(content_rect.x), int(y), color) + + if (is_hover and is_released) or rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE): + return 1 + + y += close_size + 40 + + # Title + title = "Pair your device to your comma account" + title_font = gui_app.font(FontWeight.NORMAL) + left_width = int(content_rect.width * 0.5 - 15) + + title_wrapped = wrap_text(title_font, title, 75, left_width) + rl.draw_text_ex(title_font, "\n".join(title_wrapped), rl.Vector2(content_rect.x, y), 75, 0.0, rl.BLACK) + y += len(title_wrapped) * 75 + 60 + + # Two columns: instructions and QR code + remaining_height = content_rect.height - (y - content_rect.y) + right_width = content_rect.width // 2 - 20 + + # Instructions + self._render_instructions(rl.Rectangle(content_rect.x, y, left_width, remaining_height)) + + # QR code + qr_size = min(right_width, content_rect.height) - 40 + qr_x = content_rect.x + left_width + 40 + (right_width - qr_size) // 2 + qr_y = content_rect.y + self._render_qr_code(rl.Rectangle(qr_x, qr_y, qr_size, qr_size)) + + return -1 + + def _render_instructions(self, rect: rl.Rectangle) -> None: + instructions = [ + "Go to https://connect.comma.ai on your phone", + "Click \"add new device\" and scan the QR code on the right", + "Bookmark connect.comma.ai to your home screen to use it like an app", + ] + + font = gui_app.font(FontWeight.BOLD) + y = rect.y + + for i, text in enumerate(instructions): + circle_radius = 25 + circle_x = rect.x + circle_radius + 15 + text_x = rect.x + circle_radius * 2 + 40 + text_width = rect.width - (circle_radius * 2 + 40) + + wrapped = wrap_text(font, text, 47, int(text_width)) + text_height = len(wrapped) * 47 + circle_y = y + text_height // 2 + + # Circle and number + rl.draw_circle(int(circle_x), int(circle_y), circle_radius, rl.Color(70, 70, 70, 255)) + number = str(i + 1) + number_width = measure_text_cached(font, number, 30).x + rl.draw_text(number, int(circle_x - number_width // 2), int(circle_y - 15), 30, rl.WHITE) + + # Text + rl.draw_text_ex(font, "\n".join(wrapped), rl.Vector2(text_x, y), 47, 0.0, rl.BLACK) + y += text_height + 50 + + def _render_qr_code(self, rect: rl.Rectangle) -> None: + if not self.qr_texture: + rl.draw_rectangle_rounded(rect, 0.1, 20, rl.Color(240, 240, 240, 255)) + error_font = gui_app.font(FontWeight.BOLD) + rl.draw_text_ex( + error_font, "QR Code Error", rl.Vector2(rect.x + 20, rect.y + rect.height // 2 - 15), 30, 0.0, rl.RED + ) + return + + source = rl.Rectangle(0, 0, self.qr_texture.width, self.qr_texture.height) + rl.draw_texture_pro(self.qr_texture, source, rect, rl.Vector2(0, 0), 0, rl.WHITE) + + def __del__(self): + if self.qr_texture and self.qr_texture.id != 0: + rl.unload_texture(self.qr_texture) + + +if __name__ == "__main__": + gui_app.init_window("pairing device") + pairing = PairingDialog() + try: + for _ in gui_app.render(): + result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + if result != -1: + break + finally: + del pairing diff --git a/selfdrive/ui/widgets/prime.py b/selfdrive/ui/widgets/prime.py new file mode 100644 index 0000000000..afdfa33e7a --- /dev/null +++ b/selfdrive/ui/widgets/prime.py @@ -0,0 +1,62 @@ +import pyray as rl + +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.label import gui_label +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.widget import Widget + + +class PrimeWidget(Widget): + """Widget for displaying comma prime subscription status""" + + PRIME_BG_COLOR = rl.Color(51, 51, 51, 255) + + def _render(self, rect): + if ui_state.prime_state.is_prime(): + self._render_for_prime_user(rect) + else: + self._render_for_non_prime_users(rect) + + def _render_for_non_prime_users(self, rect: rl.Rectangle): + """Renders the advertisement for non-Prime users.""" + + rl.draw_rectangle_rounded(rect, 0.02, 10, self.PRIME_BG_COLOR) + + # Layout + x, y = rect.x + 80, rect.y + 90 + w = rect.width - 160 + + # Title + gui_label(rl.Rectangle(x, y, w, 90), "Upgrade Now", 75, font_weight=FontWeight.BOLD) + + # Description with wrapping + desc_y = y + 140 + font = gui_app.font(FontWeight.LIGHT) + wrapped_text = "\n".join(wrap_text(font, "Become a comma prime member at connect.comma.ai", 56, int(w))) + text_size = measure_text_cached(font, wrapped_text, 56) + rl.draw_text_ex(font, wrapped_text, rl.Vector2(x, desc_y), 56, 0, rl.Color(255, 255, 255, 255)) + + # Features section + features_y = desc_y + text_size.y + 50 + gui_label(rl.Rectangle(x, features_y, w, 50), "PRIME FEATURES:", 41, font_weight=FontWeight.BOLD) + + # Feature list + features = ["Remote access", "24/7 LTE connectivity", "1 year of drive storage", "Remote snapshots"] + for i, feature in enumerate(features): + item_y = features_y + 80 + i * 65 + gui_label(rl.Rectangle(x, item_y, 50, 60), "✓", 50, color=rl.Color(70, 91, 234, 255)) + gui_label(rl.Rectangle(x + 60, item_y, w - 60, 60), feature, 50) + + def _render_for_prime_user(self, rect: rl.Rectangle): + """Renders the prime user widget with subscription status.""" + + rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 230), 0.02, 10, self.PRIME_BG_COLOR) + + x = rect.x + 56 + y = rect.y + 40 + + font = gui_app.font(FontWeight.BOLD) + rl.draw_text_ex(font, "✓ SUBSCRIBED", rl.Vector2(x, y), 41, 0, rl.Color(134, 255, 78, 255)) + rl.draw_text_ex(font, "comma prime", rl.Vector2(x, y + 61), 75, 0, rl.WHITE) diff --git a/selfdrive/ui/widgets/setup.py b/selfdrive/ui/widgets/setup.py new file mode 100644 index 0000000000..4fd56aa522 --- /dev/null +++ b/selfdrive/ui/widgets/setup.py @@ -0,0 +1,94 @@ +import pyray as rl +from openpilot.selfdrive.ui.lib.prime_state import PrimeType +from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.button import gui_button, ButtonStyle +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.lib.widget import Widget + + +class SetupWidget(Widget): + def __init__(self): + super().__init__() + self._open_settings_callback = None + self._pairing_dialog: PairingDialog | None = None + + def set_open_settings_callback(self, callback): + self._open_settings_callback = callback + + def _render(self, rect: rl.Rectangle): + if ui_state.prime_state.get_type() == PrimeType.UNPAIRED: + self._render_registration(rect) + else: + self._render_firehose_prompt(rect) + + def _render_registration(self, rect: rl.Rectangle): + """Render registration prompt.""" + + rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 590), 0.02, 20, rl.Color(51, 51, 51, 255)) + + x = rect.x + 64 + y = rect.y + 48 + w = rect.width - 128 + + # Title + font = gui_app.font(FontWeight.BOLD) + rl.draw_text_ex(font, "Finish Setup", rl.Vector2(x, y), 75, 0, rl.WHITE) + y += 113 # 75 + 38 spacing + + # Description + desc = "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer." + light_font = gui_app.font(FontWeight.LIGHT) + wrapped = wrap_text(light_font, desc, 50, int(w)) + for line in wrapped: + rl.draw_text_ex(light_font, line, rl.Vector2(x, y), 50, 0, rl.WHITE) + y += 50 + + button_rect = rl.Rectangle(x, y + 50, w, 128) + if gui_button(button_rect, "Pair device", button_style=ButtonStyle.PRIMARY): + self._show_pairing() + + def _render_firehose_prompt(self, rect: rl.Rectangle): + """Render firehose prompt widget.""" + + rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width, 450), 0.02, 20, rl.Color(51, 51, 51, 255)) + + # Content margins (56, 40, 56, 40) + x = rect.x + 56 + y = rect.y + 40 + w = rect.width - 112 + spacing = 42 + + # Title with fire emojis + title_font = gui_app.font(FontWeight.MEDIUM) + title_text = "Firehose Mode" + rl.draw_text_ex(title_font, title_text, rl.Vector2(x, y), 64, 0, rl.WHITE) + y += 64 + spacing + + # Description + desc_font = gui_app.font(FontWeight.NORMAL) + desc_text = "Maximize your training data uploads to improve openpilot's driving models." + wrapped_desc = wrap_text(desc_font, desc_text, 40, int(w)) + + for line in wrapped_desc: + rl.draw_text_ex(desc_font, line, rl.Vector2(x, y), 40, 0, rl.WHITE) + y += 40 + + y += spacing + + # Open button + button_height = 48 + 64 # font size + padding + button_rect = rl.Rectangle(x, y, w, button_height) + if gui_button(button_rect, "Open", button_style=ButtonStyle.PRIMARY): + if self._open_settings_callback: + self._open_settings_callback() + + def _show_pairing(self): + if not self._pairing_dialog: + self._pairing_dialog = PairingDialog() + gui_app.set_modal_overlay(self._pairing_dialog, lambda result: setattr(self, '_pairing_dialog', None)) + + def __del__(self): + if self._pairing_dialog: + del self._pairing_dialog diff --git a/selfdrive/ui/widgets/ssh_key.py b/selfdrive/ui/widgets/ssh_key.py new file mode 100644 index 0000000000..418867c86f --- /dev/null +++ b/selfdrive/ui/widgets/ssh_key.py @@ -0,0 +1,128 @@ +import pyray as rl +import requests +import threading +import copy +from enum import Enum + +from openpilot.common.params import Params +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.button import gui_button, ButtonStyle +from openpilot.system.ui.lib.list_view import ( + ItemAction, + ListItem, + BUTTON_HEIGHT, + BUTTON_BORDER_RADIUS, + BUTTON_FONT_SIZE, + BUTTON_WIDTH, +) +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.widget import DialogResult +from openpilot.system.ui.widgets.confirm_dialog import alert_dialog +from openpilot.system.ui.widgets.keyboard import Keyboard + + +class SshKeyActionState(Enum): + LOADING = "LOADING" + ADD = "ADD" + REMOVE = "REMOVE" + + +class SshKeyAction(ItemAction): + HTTP_TIMEOUT = 15 # seconds + MAX_WIDTH = 500 + + def __init__(self): + super().__init__(self.MAX_WIDTH, True) + + self._keyboard = Keyboard() + self._params = Params() + self._error_message: str = "" + self._text_font = gui_app.font(FontWeight.MEDIUM) + + self._refresh_state() + + def _refresh_state(self): + self._username = self._params.get("GithubUsername", "") + self._state = SshKeyActionState.REMOVE if self._params.get("GithubSshKeys") else SshKeyActionState.ADD + + def _render(self, rect: rl.Rectangle) -> bool: + # Show error dialog if there's an error + if self._error_message: + message = copy.copy(self._error_message) + gui_app.set_modal_overlay(lambda: alert_dialog(message)) + self._username = "" + self._error_message = "" + + # Draw username if exists + if self._username: + text_size = measure_text_cached(self._text_font, self._username, BUTTON_FONT_SIZE) + rl.draw_text_ex( + self._text_font, + self._username, + (rect.x + rect.width - BUTTON_WIDTH - text_size.x - 30, rect.y + (rect.height - text_size.y) / 2), + BUTTON_FONT_SIZE, + 1.0, + rl.WHITE, + ) + + # Draw button + if gui_button( + rl.Rectangle( + rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT + ), + self._state.value, + is_enabled=self._state != SshKeyActionState.LOADING, + border_radius=BUTTON_BORDER_RADIUS, + font_size=BUTTON_FONT_SIZE, + button_style=ButtonStyle.LIST_ACTION, + ): + self._handle_button_click() + return True + return False + + def _handle_button_click(self): + if self._state == SshKeyActionState.ADD: + self._keyboard.clear() + self._keyboard.set_title("Enter your GitHub username") + gui_app.set_modal_overlay(self._keyboard, callback=self._on_username_submit) + elif self._state == SshKeyActionState.REMOVE: + self._params.remove("GithubUsername") + self._params.remove("GithubSshKeys") + self._refresh_state() + + def _on_username_submit(self, result: DialogResult): + if result != DialogResult.CONFIRM: + return + + username = self._keyboard.text.strip() + if not username: + return + + self._state = SshKeyActionState.LOADING + threading.Thread(target=lambda: self._fetch_ssh_key(username), daemon=True).start() + + def _fetch_ssh_key(self, username: str): + try: + url = f"https://github.com/{username}.keys" + response = requests.get(url, timeout=self.HTTP_TIMEOUT) + response.raise_for_status() + keys = response.text.strip() + if not keys: + raise requests.exceptions.HTTPError("No SSH keys found") + + # Success - save keys + self._params.put("GithubUsername", username) + self._params.put("GithubSshKeys", keys) + self._state = SshKeyActionState.REMOVE + self._username = username + + except requests.exceptions.Timeout: + self._error_message = "Request timed out" + self._state = SshKeyActionState.ADD + except Exception: + self._error_message = f"No SSH keys found for user '{username}'" + self._state = SshKeyActionState.ADD + + +def ssh_key_item(title: str, description: str): + return ListItem(title=title, description=description, action_item=SshKeyAction()) diff --git a/sunnypilot/mapd/__init__.py b/sunnypilot/mapd/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sunnypilot/mapd/live_map_data/__init__.py b/sunnypilot/mapd/live_map_data/__init__.py new file mode 100644 index 0000000000..557ad06515 --- /dev/null +++ b/sunnypilot/mapd/live_map_data/__init__.py @@ -0,0 +1,22 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from openpilot.common.swaglog import cloudlog + +LOOK_AHEAD_HORIZON_TIME = 15. # s. Time horizon for look ahead of turn speed sections to provide on liveMapDataSP msg. +_DEBUG = False +_CLOUDLOG_DEBUG = False +ROAD_NAME_TIMEOUT = 30 # secs +R = 6373000.0 # approximate radius of earth in mts +QUERY_RADIUS = 3000 # mts. Radius to use on OSM data queries. +QUERY_RADIUS_OFFLINE = 2250 # mts. Radius to use on offline OSM data queries. + + +def get_debug(msg, log_to_cloud=True): + if _CLOUDLOG_DEBUG and log_to_cloud: + cloudlog.debug(msg) + if _DEBUG: + print(msg) diff --git a/sunnypilot/mapd/live_map_data/base_map_data.py b/sunnypilot/mapd/live_map_data/base_map_data.py new file mode 100644 index 0000000000..536d7720b7 --- /dev/null +++ b/sunnypilot/mapd/live_map_data/base_map_data.py @@ -0,0 +1,76 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import time +from abc import abstractmethod, ABC + +from cereal import messaging +from openpilot.common.gps import get_gps_location_service +from openpilot.common.params import Params +from openpilot.sunnypilot.navd.helpers import Coordinate, coordinate_from_param + + +class BaseMapData(ABC): + def __init__(self): + self.params = Params() + + self.gps_location_service = get_gps_location_service(self.params) + self.sm = messaging.SubMaster(['livePose', 'carControl'] + [self.gps_location_service]) + self.pm = messaging.PubMaster(['liveMapDataSP']) + + self.last_position = coordinate_from_param("LastGPSPosition", self.params) + self.last_altitude = None + + @abstractmethod + def update_location(self) -> None: + pass + + @abstractmethod + def get_current_speed_limit(self) -> float: + pass + + @abstractmethod + def get_next_speed_limit_and_distance(self) -> tuple[float, float]: + pass + + @abstractmethod + def get_current_road_name(self) -> str: + pass + + def get_current_location(self) -> None: + gps = self.sm[self.gps_location_service] + + # ignore the message if the fix is invalid + gps_ok = self.sm.updated[self.gps_location_service] or (time.monotonic() - self.sm.logMonoTime[self.gps_location_service] / 1e9) > 2.0 + if not gps_ok and self.sm['livePose'].inputsOK: + return None + + # livePose has these data, but aren't on cereal + self.last_position = Coordinate(gps.latitude, gps.longitude) + self.last_altitude = gps.altitude + + def publish(self) -> None: + speed_limit = self.get_current_speed_limit() + next_speed_limit, next_speed_limit_distance = self.get_next_speed_limit_and_distance() + + mapd_sp_send = messaging.new_message('liveMapDataSP') + mapd_sp_send.valid = self.sm.all_checks(service_list=[self.gps_location_service, 'livePose']) + live_map_data = mapd_sp_send.liveMapDataSP + + live_map_data.speedLimitValid = bool(speed_limit > 0) + live_map_data.speedLimit = speed_limit + live_map_data.speedLimitAheadValid = bool(next_speed_limit > 0) + live_map_data.speedLimitAhead = next_speed_limit + live_map_data.speedLimitAheadDistance = next_speed_limit_distance + live_map_data.roadName = self.get_current_road_name() + + self.pm.send('liveMapDataSP', mapd_sp_send) + + def tick(self) -> None: + self.sm.update() + self.get_current_location() + self.update_location() + self.publish() diff --git a/sunnypilot/mapd/live_map_data/debug.py b/sunnypilot/mapd/live_map_data/debug.py new file mode 100644 index 0000000000..da9f4d7771 --- /dev/null +++ b/sunnypilot/mapd/live_map_data/debug.py @@ -0,0 +1,57 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +# DISCLAIMER: This code is intended principally for development and debugging purposes. +# Although it provides a standalone entry point to the program, users should refer +# to the actual implementations for consumption. Usage outside of development scenarios +# is not advised and could lead to unpredictable results. + +import threading +import traceback + +from cereal import messaging +from openpilot.common.gps import get_gps_location_service +from openpilot.common.params import Params +from openpilot.common.realtime import config_realtime_process +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver +from openpilot.sunnypilot.mapd.live_map_data import get_debug + + +def excepthook(args): + get_debug(f'MapD: Threading exception:\n{args}') + traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback) + + +def live_map_data_sp_thread(): + config_realtime_process([0, 1, 2, 3], 5) + + params = Params() + gps_location_service = get_gps_location_service(params) + + while True: + live_map_data_sp_thread_debug(gps_location_service) + + +def live_map_data_sp_thread_debug(gps_location_service): + _sub_master = messaging.SubMaster(['carState', 'livePose', 'liveMapDataSP', 'longitudinalPlanSP', gps_location_service]) + _sub_master.update() + + v_ego = _sub_master['carState'].vEgo + long_spl = _sub_master['longitudinalPlanSP'].speedLimit + _policy = Policy.car_state_priority + _resolver = SpeedLimitResolver(_policy) + _speed_limit, _distance, _source = _resolver.resolve(v_ego, long_spl, _sub_master) + print(_speed_limit, _distance, _source, " <-> ", long_spl) + + +def main(): + threading.excepthook = excepthook + live_map_data_sp_thread() + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/mapd/live_map_data/osm_map_data.py b/sunnypilot/mapd/live_map_data/osm_map_data.py new file mode 100644 index 0000000000..5f74a5d011 --- /dev/null +++ b/sunnypilot/mapd/live_map_data/osm_map_data.py @@ -0,0 +1,51 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import json +import platform + +from openpilot.common.params import Params +from openpilot.sunnypilot.mapd.live_map_data.base_map_data import BaseMapData +from openpilot.sunnypilot.navd.helpers import Coordinate + + +class OsmMapData(BaseMapData): + def __init__(self): + super().__init__() + self.params = Params() + self.mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else self.params + + def update_location(self) -> None: + if self.last_position is None or self.last_altitude is None: + return + + params = { + "latitude": self.last_position.latitude, + "longitude": self.last_position.longitude, + "altitude": self.last_altitude, + } + + self.mem_params.put("LastGPSPosition", json.dumps(params)) + + def get_current_speed_limit(self) -> float: + return float(self.mem_params.get("MapSpeedLimit", encoding='utf8') or 0.0) + + def get_current_road_name(self) -> str: + return self.mem_params.get("RoadName", encoding='utf8') or "" + + def get_next_speed_limit_and_distance(self) -> tuple[float, float]: + next_speed_limit_section_str = self.mem_params.get("NextMapSpeedLimit", encoding='utf8') + next_speed_limit_section = json.loads(next_speed_limit_section_str) if next_speed_limit_section_str else {} + next_speed_limit = next_speed_limit_section.get('speedlimit', 0.0) + next_speed_limit_latitude = next_speed_limit_section.get('latitude') + next_speed_limit_longitude = next_speed_limit_section.get('longitude') + next_speed_limit_distance = 0.0 + + if next_speed_limit_latitude and next_speed_limit_longitude: + next_speed_limit_coordinates = Coordinate(next_speed_limit_latitude, next_speed_limit_longitude) + next_speed_limit_distance = (self.last_position or Coordinate(0, 0)).distance_to(next_speed_limit_coordinates) + + return next_speed_limit, next_speed_limit_distance diff --git a/sunnypilot/mapd/live_map_data/standalone.py b/sunnypilot/mapd/live_map_data/standalone.py new file mode 100644 index 0000000000..f73ecc7724 --- /dev/null +++ b/sunnypilot/mapd/live_map_data/standalone.py @@ -0,0 +1,42 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +# DISCLAIMER: This code is intended principally for development and debugging purposes. +# Although it provides a standalone entry point to the program, users should refer +# to the actual implementations for consumption. Usage outside of development scenarios +# is not advised and could lead to unpredictable results. + +import threading +import traceback + +from openpilot.common.realtime import Ratekeeper, config_realtime_process +from openpilot.sunnypilot.mapd.live_map_data import get_debug +from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData + + +def excepthook(args): + get_debug(f'MapD: Threading exception:\n{args}') + traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback) + + +def live_map_data_sp_thread(): + config_realtime_process([0, 1, 2, 3], 5) + + live_map_sp = OsmMapData() + rk = Ratekeeper(1, print_delay_threshold=None) + + while True: + live_map_sp.tick() + rk.keep_time() + + +def main(): + threading.excepthook = excepthook + live_map_data_sp_thread() + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/mapd/mapd_installer.py b/sunnypilot/mapd/mapd_installer.py new file mode 100755 index 0000000000..a151dd4807 --- /dev/null +++ b/sunnypilot/mapd/mapd_installer.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import logging +import os +import stat +import time +import traceback +import requests +from pathlib import Path +from urllib.request import urlopen + +from cereal import messaging +from openpilot.common.params import Params +from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH, MAPD_BIN_DIR +from openpilot.system.hardware.hw import Paths +from openpilot.common.spinner import Spinner +from openpilot.system.version import is_prebuilt +import openpilot.system.sentry as sentry + +VERSION = 'v1.9.0' +URL = f"https://github.com/pfeiferj/openpilot-mapd/releases/download/{VERSION}/mapd" + + +class MapdInstallManager: + def __init__(self, spinner_ref: Spinner): + self._spinner = spinner_ref + + def download(self) -> None: + self.ensure_directories_exist() + self._download_file() + self.update_installed_version(VERSION) + + def check_and_download(self) -> None: + if self.download_needed(): + self.download() + + @staticmethod + def download_needed() -> bool: + return not os.path.exists(MAPD_PATH) or MapdInstallManager.get_installed_version() != VERSION + + @staticmethod + def ensure_directories_exist() -> None: + if not os.path.exists(Paths.mapd_root()): + os.makedirs(Paths.mapd_root()) + if not os.path.exists(MAPD_BIN_DIR): + os.makedirs(MAPD_BIN_DIR) + + @staticmethod + def _safe_write_and_set_executable(file_path: Path, content: bytes) -> None: + with open(file_path, 'wb') as output: + output.write(content) + output.flush() + os.fsync(output.fileno()) + current_permissions = stat.S_IMODE(os.lstat(file_path).st_mode) + os.chmod(file_path, current_permissions | stat.S_IEXEC) + + def _download_file(self, num_retries=5) -> None: + temp_file = Path(MAPD_PATH + ".tmp") + download_timeout = 60 + for cnt in range(num_retries): + try: + response = requests.get(URL, stream=True, timeout=download_timeout) + response.raise_for_status() + self._safe_write_and_set_executable(temp_file, response.content) + # No exceptions encountered. Safe to replace original file. + temp_file.replace(MAPD_PATH) + return + except requests.exceptions.ReadTimeout: + self._spinner.update(f"ReadTimeout caught. Timeout is [{download_timeout}]. Retrying download... [{cnt}]") + time.sleep(0.5) + except requests.exceptions.RequestException as e: + self._spinner.update(f"RequestException caught: {e}. Retrying download... [{cnt}]") + time.sleep(0.5) + + # Delete temp file if the process was not successful. + if temp_file.exists(): + temp_file.unlink() + logging.error("Failed to download file after all retries") + + @staticmethod + def update_installed_version(version: str) -> None: + Params().put("MapdVersion", version) + + @staticmethod + def get_installed_version() -> str: + return Params().get("MapdVersion", encoding="utf-8") or "" + + def wait_for_internet_connection(self, return_on_failure: bool = False) -> bool: + max_retries = 10 + for retries in range(max_retries + 1): + self._spinner.update(f"Waiting for internet connection... [{retries}/{max_retries}]") + time.sleep(2) + try: + _ = urlopen('https://sentry.io', timeout=10) + return True + except Exception as e: + print(f'Wait for internet failed: {e}') + if return_on_failure and retries == max_retries: + return False + + return False + + def non_prebuilt_install(self) -> None: + sm = messaging.SubMaster(['deviceState']) + metered = sm['deviceState'].networkMetered + + if metered: + self._spinner.update("Can't proceed with mapd install since network is metered!") + time.sleep(5) + return + + try: + self.ensure_directories_exist() + if not self.download_needed(): + self._spinner.update("Mapd is good!") + time.sleep(0.1) + return + + if self.wait_for_internet_connection(return_on_failure=True): + self._spinner.update(f"Downloading pfeiferj's mapd [{install_manager.get_installed_version()}] => [{VERSION}].") + time.sleep(0.1) + self.check_and_download() + self._spinner.close() + + except Exception: + for i in range(6): + self._spinner.update("Failed to download OSM maps won't work until properly downloaded!" + + "Try again manually rebooting. " + + f"Boot will continue in {5 - i}s...") + time.sleep(1) + + sentry.init(sentry.SentryProject.SELFDRIVE) + traceback.print_exc() + sentry.capture_exception() + + +if __name__ == "__main__": + spinner = Spinner() + install_manager = MapdInstallManager(spinner) + install_manager.ensure_directories_exist() + if is_prebuilt(): + debug_msg = f"[DEBUG] This is prebuilt, no mapd install required. VERSION: [{VERSION}], Param [{install_manager.get_installed_version()}]" + spinner.update(debug_msg) + install_manager.update_installed_version(VERSION) + else: + spinner.update(f"Checking if mapd is installed and valid. Prebuilt [{is_prebuilt()}]") + install_manager.non_prebuilt_install() diff --git a/sunnypilot/mapd/mapd_manager.py b/sunnypilot/mapd/mapd_manager.py new file mode 100755 index 0000000000..342ba3fe7b --- /dev/null +++ b/sunnypilot/mapd/mapd_manager.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import json +import time +import platform +import os +import glob +import shutil + +from openpilot.common.basedir import BASEDIR +from openpilot.common.params import Params +from openpilot.common.realtime import Ratekeeper, config_realtime_process +from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert +from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData +from openpilot.system.hardware.hw import Paths + +# PFEIFER - MAPD {{ +params = Params() +mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else params +# }} PFEIFER - MAPD + +MAPD_BIN_DIR = os.path.join(BASEDIR, 'third_party/mapd_pfeiferj') +MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd') + + +def get_files_for_cleanup() -> list[str]: + paths = [ + f"{Paths.mapd_root()}/db", + f"{Paths.mapd_root()}/v*" + ] + files_to_remove = [] + for path in paths: + if os.path.exists(path): + files = glob.glob(path + '/**', recursive=True) + files_to_remove.extend(files) + # check for version and mapd files + if not os.path.isfile(MAPD_PATH): + files_to_remove.append(MAPD_PATH) + return files_to_remove + + +def cleanup_old_osm_data(files_to_remove: list[str]) -> None: + for file in files_to_remove: + # Remove trailing slash if path is file + if file.endswith('/') and os.path.isfile(file[:-1]): + file = file[:-1] + # Try to remove as file or symbolic link first + if os.path.islink(file) or os.path.isfile(file): + os.remove(file) + elif os.path.isdir(file): # If it's a directory + shutil.rmtree(file, ignore_errors=False) + + +def request_refresh_osm_location_data(nations: list[str], states: list[str] = None) -> None: + params.put("OsmDownloadedDate", str(time.time())) + params.put_bool("OsmDbUpdatesCheck", False) + + osm_download_locations = json.dumps({ + "nations": nations, + "states": states or [] + }) + + print(f"Downloading maps for {osm_download_locations}") + mem_params.put("OSMDownloadLocations", osm_download_locations) + + +def filter_nations_and_states(nations: list[str], states: list[str] = None) -> tuple[list[str], list[str]]: + """Filters and prepares nation and state data for OSM map download. + + If the nation is 'US' and a specific state is provided, the nation 'US' is removed from the list. + If the nation is 'US' and the state is 'All', the 'All' is removed from the list. + The idea behind these filters is that if a specific state in the US is provided, + there's no need to download map data for the entire US. Conversely, + if the state is unspecified (i.e., 'All'), we intend to download map data for the whole US, + and 'All' isn't a valid state name, so it's removed. + + Parameters: + nations (list): A list of nations for which the map data is to be downloaded. + states (list, optional): A list of states for which the map data is to be downloaded. Defaults to None. + + Returns: + tuple: Two lists. The first list is filtered nations and the second list is filtered states. + """ + + if "US" in nations and states and not any(x.lower() == "all" for x in states): + # If a specific state in the US is provided, remove 'US' from nations + nations.remove("US") + elif "US" in nations and states and any(x.lower() == "all" for x in states): + # If 'All' is provided as a state (case invariant), remove those instances from states + states = [x for x in states if x.lower() != "all"] + elif "US" not in nations and states and any(x.lower() == "all" for x in states): + states.remove("All") + return nations, states or [] + + +def update_osm_db() -> None: + # last_downloaded_date = float(params.get('OsmDownloadedDate', encoding='utf-8') or 0.0) + # if params.get_bool("OsmDbUpdatesCheck") or time.time() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60 + if params.get_bool("OsmDbUpdatesCheck"): + cleanup_old_osm_data(get_files_for_cleanup()) + country = params.get('OsmLocationName', encoding='utf-8') + state = params.get('OsmStateName', encoding='utf-8') or "All" + filtered_nations, filtered_states = filter_nations_and_states([country], [state]) + request_refresh_osm_location_data(filtered_nations, filtered_states) + + if not mem_params.get("OSMDownloadBounds"): + mem_params.put("OSMDownloadBounds", "") + + if not mem_params.get("LastGPSPosition"): + mem_params.put("LastGPSPosition", "{}") + + +def main_thread(): + config_realtime_process([0, 1, 2, 3], 5) + + rk = Ratekeeper(1, print_delay_threshold=None) + live_map_sp = OsmMapData() + + # Create folder needed for OSM + try: + os.mkdir(Paths.mapd_root()) + except FileExistsError: + pass + except PermissionError: + cloudlog.exception(f"mapd: failed to make {Paths.mapd_root()}") + + while True: + show_alert = get_files_for_cleanup() and params.get_bool("OsmLocal") + set_offroad_alert("Offroad_OSMUpdateRequired", show_alert, "This alert will be cleared when new maps are downloaded.") + + update_osm_db() + live_map_sp.tick() + rk.keep_time() + + +def main(): + main_thread() + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py index 2ac3f5700e..d4bd96ff66 100755 --- a/sunnypilot/modeld/modeld.py +++ b/sunnypilot/modeld/modeld.py @@ -25,6 +25,7 @@ from openpilot.sunnypilot.modeld.parse_model_outputs import Parser from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants +from openpilot.sunnypilot.models.modeld_lagd import ModeldLagd from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext @@ -201,8 +202,9 @@ def main(demo=False): cloudlog.info("modeld got CarParams: %s", CP.brand) + modeld_lagd = ModeldLagd() + # Enable lagd support for sunnypilot modeld - steer_delay = sm["liveDelay"].lateralDelay + model.LAT_SMOOTH_SECONDS long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS prev_action = log.ModelDataV2.Action() @@ -246,6 +248,8 @@ def main(demo=False): v_ego = sm["carState"].vEgo is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId + steer_delay = modeld_lagd.lagd_main(CP, sm, model) + if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] diff --git a/sunnypilot/modeld_v2/SConscript b/sunnypilot/modeld_v2/SConscript index 4ade1469f0..bb4c8d4a50 100644 --- a/sunnypilot/modeld_v2/SConscript +++ b/sunnypilot/modeld_v2/SConscript @@ -13,7 +13,6 @@ common_src = [ "transforms/transform.cc", ] - # OpenCL is a framework on Mac if arch == "Darwin": frameworks += ['OpenCL'] @@ -29,4 +28,3 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor cython_libs = envCython["LIBS"] + libs commonmodel_lib = lenv.Library('commonmodel', common_src) lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks) - diff --git a/sunnypilot/modeld_v2/fill_model_msg.py b/sunnypilot/modeld_v2/fill_model_msg.py index 411b8177f3..4d04d6e5ec 100644 --- a/sunnypilot/modeld_v2/fill_model_msg.py +++ b/sunnypilot/modeld_v2/fill_model_msg.py @@ -118,30 +118,14 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D # action (includes lateral planning now) modelV2.action = action - # times at X_IDXS according to model plan - PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N - PLAN_T_IDXS[0] = 0.0 - plan_x = net_output_data['plan'][0,:,Plan.POSITION][:,0].tolist() - for xidx in range(1, ModelConstants.IDX_N): - tidx = 0 - # increment tidx until we find an element that's further away than the current xidx - while tidx < ModelConstants.IDX_N - 1 and plan_x[tidx+1] < ModelConstants.X_IDXS[xidx]: - tidx += 1 - if tidx == ModelConstants.IDX_N - 1: - # if the Plan doesn't extend far enough, set plan_t to the max value (10s), then break - PLAN_T_IDXS[xidx] = ModelConstants.T_IDXS[ModelConstants.IDX_N - 1] - break - # interpolate to find `t` for the current xidx - current_x_val = plan_x[tidx] - next_x_val = plan_x[tidx+1] - p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs(next_x_val - current_x_val) > 1e-9 else float('nan') - PLAN_T_IDXS[xidx] = p * ModelConstants.T_IDXS[tidx+1] + (1 - p) * ModelConstants.T_IDXS[tidx] + # times at X_IDXS of edges and lines aren't used + LINE_T_IDXS: list[float] = [] # lane lines modelV2.init('laneLines', 4) for i in range(4): lane_line = modelV2.laneLines[i] - fill_xyzt(lane_line, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1]) + fill_xyzt(lane_line, LINE_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1]) modelV2.laneLineStds = net_output_data['lane_lines_stds'][0,:,0,0].tolist() modelV2.laneLineProbs = net_output_data['lane_lines_prob'][0,1::2].tolist() @@ -151,7 +135,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D modelV2.init('roadEdges', 2) for i in range(2): road_edge = modelV2.roadEdges[i] - fill_xyzt(road_edge, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1]) + fill_xyzt(road_edge, LINE_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1]) modelV2.roadEdgeStds = net_output_data['road_edges_stds'][0,:,0,0].tolist() # leads diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index 4c8acafcad..5b9c5ef9e5 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -23,6 +23,7 @@ from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFr from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants from openpilot.sunnypilot.models.helpers import get_active_bundle +from openpilot.sunnypilot.models.modeld_lagd import ModeldLagd from openpilot.sunnypilot.models.runners.helpers import get_model_runner PROCESS_NAME = "selfdrive.modeld.modeld" @@ -61,7 +62,7 @@ class ModelState: self.MIN_LAT_CONTROL_SPEED = 0.3 buffer_length = 5 if self.model_runner.is_20hz else 2 - self.frames = {'input_imgs': DrivingModelFrame(context, buffer_length), 'big_input_imgs': DrivingModelFrame(context, buffer_length)} + self.frames = {name: DrivingModelFrame(context, buffer_length) for name in self.model_runner.vision_input_names} self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32) # img buffers are managed in openCL transform code @@ -85,7 +86,7 @@ class ModelState: self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1, self.numpy_inputs['desire'].shape[2]) - def run(self, buf: VisionBuf, wbuf: VisionBuf, transform: np.ndarray, transform_wide: np.ndarray, + def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: # Model decides when action is completed, so desire input is just a pulse triggered on rising edge inputs['desire'][0] = 0 @@ -109,8 +110,7 @@ class ModelState: if key in inputs and key not in ['desire']: self.numpy_inputs[key][:] = inputs[key] - imgs_cl = {'input_imgs': self.frames['input_imgs'].prepare(buf, transform.flatten()), - 'big_input_imgs': self.frames['big_input_imgs'].prepare(wbuf, transform_wide.flatten())} + imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.model_runner.vision_input_names} # Prepare inputs using the model runner self.model_runner.prepare_inputs(imgs_cl, self.numpy_inputs, self.frames) @@ -238,6 +238,9 @@ def main(demo=False): CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) cloudlog.info("modeld got CarParams: %s", CP.brand) + + modeld_lagd = ModeldLagd() + # TODO Move smooth seconds to action function long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS prev_action = log.ModelDataV2.Action() @@ -282,7 +285,7 @@ def main(demo=False): is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) - steer_delay = sm["liveDelay"].lateralDelay + model.LAT_SMOOTH_SECONDS + steer_delay = modeld_lagd.lagd_main(CP, sm, model) if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] @@ -311,6 +314,8 @@ def main(demo=False): if prepare_only: cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames") + bufs = {name: buf_extra if 'big' in name else buf_main for name in model.model_runner.vision_input_names} + transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.model_runner.vision_input_names} inputs:dict[str, np.ndarray] = { 'desire': vec_desire, 'traffic_convention': traffic_convention, @@ -320,7 +325,7 @@ def main(demo=False): inputs['lateral_control_params'] = np.array([v_ego, steer_delay], dtype=np.float32) mt1 = time.perf_counter() - model_output = model.run(buf_main, buf_extra, model_transform_main, model_transform_extra, inputs, prepare_only) + model_output = model.run(bufs, transforms, inputs, prepare_only) mt2 = time.perf_counter() model_execution_time = mt2 - mt1 diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index 87f00ea5fa..a614447ceb 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -19,7 +19,7 @@ from openpilot.system.hardware import PC from openpilot.system.hardware.hw import Paths from pathlib import Path -CURRENT_SELECTOR_VERSION = 5 +CURRENT_SELECTOR_VERSION = 6 REQUIRED_MIN_SELECTOR_VERSION = 5 USE_ONNX = os.getenv('USE_ONNX', PC) diff --git a/sunnypilot/models/modeld_lagd.py b/sunnypilot/models/modeld_lagd.py new file mode 100644 index 0000000000..9e538123a8 --- /dev/null +++ b/sunnypilot/models/modeld_lagd.py @@ -0,0 +1,26 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog + + +class ModeldLagd: + def __init__(self): + self.params = Params() + + def lagd_main(self, CP, sm, model): + if self.params.get_bool("LagdToggle"): + lateral_delay = sm["liveDelay"].lateralDelay + lat_smooth = model.LAT_SMOOTH_SECONDS + result = lateral_delay + lat_smooth + cloudlog.debug(f"LAGD USING LIVE DELAY: {lateral_delay:.3f} + {lat_smooth:.3f} = {result:.3f}") + return result + steer_actuator_delay = CP.steerActuatorDelay + lat_smooth = model.LAT_SMOOTH_SECONDS + result = (steer_actuator_delay + 0.2) + lat_smooth + cloudlog.debug(f"LAGD USING STEER ACTUATOR: {steer_actuator_delay:.3f} + 0.2 + {lat_smooth:.3f} = {result:.3f}") + return result diff --git a/sunnypilot/models/runners/model_runner.py b/sunnypilot/models/runners/model_runner.py index c210d5b87e..7124662791 100644 --- a/sunnypilot/models/runners/model_runner.py +++ b/sunnypilot/models/runners/model_runner.py @@ -12,8 +12,15 @@ CUSTOM_MODEL_PATH = Paths.model_root() # Set QCOM environment variable for TICI devices, potentially enabling hardware acceleration -if TICI: +USBGPU = "USBGPU" in os.environ +if USBGPU: + os.environ['AMD'] = '1' + os.environ['AMD_IFACE'] = 'USB' +elif TICI: os.environ['QCOM'] = '1' +else: + os.environ['LLVM'] = '1' + os.environ['JIT'] = '2' # TODO: This may cause issues class ModelData: @@ -132,6 +139,13 @@ class ModelRunner(ModularRunner): return self._model_data.output_slices raise ValueError("Model data is not available. Ensure the model is loaded correctly.") + @property + def vision_input_names(self) -> list[str]: + """Returns the list of vision input names from the input shapes.""" + if self._model_data: + return list(self._model_data.input_shapes.keys()) + raise ValueError("Model data is not available. Ensure the model is loaded correctly.") + @abstractmethod def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: """ diff --git a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py index 2e4fd45294..270890cace 100644 --- a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py +++ b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py @@ -54,6 +54,11 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny self.input_to_dtype[name] = info[2] # dtype self.input_to_device[name] = info[3] # device + @property + def vision_input_names(self) -> list[str]: + """Returns the list of vision input names from the input shapes.""" + return [name for name in self.input_shapes.keys() if 'img' in name] + def prepare_vision_inputs(self, imgs_cl: CLMemDict, frames: FrameDict): """Prepares vision (image) inputs as Tinygrad Tensors.""" for key in imgs_cl: @@ -109,6 +114,11 @@ class TinygradSplitRunner(ModelRunner): vision_output = self.vision_runner.run_model() return {**policy_output, **vision_output} # Combine results + @property + def vision_input_names(self) -> list[str]: + """Returns the list of vision input names from the vision runner.""" + return list(self.vision_runner.vision_input_names) + @property def input_shapes(self) -> ShapeDict: """Returns the combined input shapes from both vision and policy models.""" diff --git a/sunnypilot/models/tests/model_hash b/sunnypilot/models/tests/model_hash index d51753107e..f65c628164 100644 --- a/sunnypilot/models/tests/model_hash +++ b/sunnypilot/models/tests/model_hash @@ -1 +1 @@ -bfbbf61606dad5d9349db246ad51a3468490d756bc61106a63fb5a220af4cdc6 \ No newline at end of file +71979b29c4bab3007de1a4265442d79f44c0eaef066af66086dddfc432709b94 \ No newline at end of file diff --git a/sunnypilot/navd/helpers.py b/sunnypilot/navd/helpers.py new file mode 100644 index 0000000000..85894fb2ab --- /dev/null +++ b/sunnypilot/navd/helpers.py @@ -0,0 +1,189 @@ +from __future__ import annotations + +import json +import math +import numpy as np +from typing import Any, cast + +from openpilot.common.conversions import Conversions +from openpilot.common.params import Params + +DIRECTIONS = ('left', 'right', 'straight') +MODIFIABLE_DIRECTIONS = ('left', 'right') + +EARTH_MEAN_RADIUS = 6371007.2 +SPEED_CONVERSIONS = { + 'km/h': Conversions.KPH_TO_MS, + 'mph': Conversions.MPH_TO_MS, +} + + +class Coordinate: + def __init__(self, latitude: float, longitude: float) -> None: + self.latitude = latitude + self.longitude = longitude + self.annotations: dict[str, float] = {} + + @classmethod + def from_mapbox_tuple(cls, t: tuple[float, float]) -> Coordinate: + return cls(t[1], t[0]) + + def as_dict(self) -> dict[str, float]: + return {'latitude': self.latitude, 'longitude': self.longitude} + + def __str__(self) -> str: + return f'Coordinate({self.latitude}, {self.longitude})' + + def __repr__(self) -> str: + return self.__str__() + + def __eq__(self, other) -> bool: + if not isinstance(other, Coordinate): + return False + return (self.latitude == other.latitude) and (self.longitude == other.longitude) + + def __sub__(self, other: Coordinate) -> Coordinate: + return Coordinate(self.latitude - other.latitude, self.longitude - other.longitude) + + def __add__(self, other: Coordinate) -> Coordinate: + return Coordinate(self.latitude + other.latitude, self.longitude + other.longitude) + + def __mul__(self, c: float) -> Coordinate: + return Coordinate(self.latitude * c, self.longitude * c) + + def dot(self, other: Coordinate) -> float: + return self.latitude * other.latitude + self.longitude * other.longitude + + def distance_to(self, other: Coordinate) -> float: + # Haversine formula + dlat = math.radians(other.latitude - self.latitude) + dlon = math.radians(other.longitude - self.longitude) + + haversine_dlat = math.sin(dlat / 2.0) + haversine_dlat *= haversine_dlat + haversine_dlon = math.sin(dlon / 2.0) + haversine_dlon *= haversine_dlon + + y = haversine_dlat \ + + math.cos(math.radians(self.latitude)) \ + * math.cos(math.radians(other.latitude)) \ + * haversine_dlon + x = 2 * math.asin(math.sqrt(y)) + return x * EARTH_MEAN_RADIUS + + +def minimum_distance(a: Coordinate, b: Coordinate, p: Coordinate): + if a.distance_to(b) < 0.01: + return a.distance_to(p) + + ap = p - a + ab = b - a + t = np.clip(ap.dot(ab) / ab.dot(ab), 0.0, 1.0) + projection = a + ab * t + return projection.distance_to(p) + + +def distance_along_geometry(geometry: list[Coordinate], pos: Coordinate) -> float: + if len(geometry) <= 2: + return geometry[0].distance_to(pos) + + # 1. Find segment that is closest to current position + # 2. Total distance is sum of distance to start of closest segment + # + all previous segments + total_distance = 0.0 + total_distance_closest = 0.0 + closest_distance = 1e9 + + for i in range(len(geometry) - 1): + d = minimum_distance(geometry[i], geometry[i + 1], pos) + + if d < closest_distance: + closest_distance = d + total_distance_closest = total_distance + geometry[i].distance_to(pos) + + total_distance += geometry[i].distance_to(geometry[i + 1]) + + return total_distance_closest + + +def coordinate_from_param(param: str, params: Params = None) -> Coordinate | None: + if params is None: + params = Params() + + json_str = params.get(param) + if json_str is None: + return None + + pos = json.loads(json_str) + if 'latitude' not in pos or 'longitude' not in pos: + return None + + return Coordinate(pos['latitude'], pos['longitude']) + + +def string_to_direction(direction: str) -> str: + for d in DIRECTIONS: + if d in direction: + if 'slight' in direction and d in MODIFIABLE_DIRECTIONS: + return 'slight' + d.capitalize() + return d + return 'none' + + +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 + + +def field_valid(dat: dict, field: str) -> bool: + return field in dat and dat[field] is not None + + +def parse_banner_instructions(banners: Any, distance_to_maneuver: float = 0.0) -> dict[str, Any] | None: + if not len(banners): + return None + + instruction = {} + + # A segment can contain multiple banners, find one that we need to show now + current_banner = banners[0] + for banner in banners: + if distance_to_maneuver < banner['distanceAlongGeometry']: + current_banner = banner + + # Only show banner when close enough to maneuver + instruction['showFull'] = distance_to_maneuver < current_banner['distanceAlongGeometry'] + + # Primary + p = current_banner['primary'] + if field_valid(p, 'text'): + instruction['maneuverPrimaryText'] = p['text'] + if field_valid(p, 'type'): + instruction['maneuverType'] = p['type'] + if field_valid(p, 'modifier'): + instruction['maneuverModifier'] = p['modifier'] + + # Secondary + if field_valid(current_banner, 'secondary'): + instruction['maneuverSecondaryText'] = current_banner['secondary']['text'] + + # Lane lines + if field_valid(current_banner, 'sub'): + lanes = [] + for component in current_banner['sub']['components']: + if component['type'] != 'lane': + continue + + lane = { + 'active': component['active'], + 'directions': [string_to_direction(d) for d in component['directions']], + } + + if field_valid(component, 'active_direction'): + lane['activeDirection'] = string_to_direction(component['active_direction']) + + lanes.append(lane) + instruction['lanes'] = lanes + + return instruction diff --git a/sunnypilot/neural_network_data b/sunnypilot/neural_network_data index 16946bfd9c..b59ab483c8 160000 --- a/sunnypilot/neural_network_data +++ b/sunnypilot/neural_network_data @@ -1 +1 @@ -Subproject commit 16946bfd9cca1a33e8b609d10ca3d1a0089d38e2 +Subproject commit b59ab483c86cd70d04de91ad878024cd66fe86b6 diff --git a/sunnypilot/selfdrive/assets/images/spinner_sunnypilot.png b/sunnypilot/selfdrive/assets/images/spinner_sunnypilot.png new file mode 100644 index 0000000000..edc5c6e5c2 --- /dev/null +++ b/sunnypilot/selfdrive/assets/images/spinner_sunnypilot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dbacb55581a5ea8cbcd5cea0560ec21d96ac7185463a40fff0f81bebf044ffa +size 23187 diff --git a/sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png b/sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png new file mode 100644 index 0000000000..1b8308a0ac --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_exit_offroad.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d44f71f7603b106986fff7835f35c4c18c4c619a29e6292f8626ae3eedd85e57 +size 7811 diff --git a/sunnypilot/selfdrive/assets/offroad/icon_map.png b/sunnypilot/selfdrive/assets/offroad/icon_map.png new file mode 100644 index 0000000000..82c0236a48 --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_map.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57a92adcf88c7223b07697f8c2b315f4f4a34b32a866284610d5250144863c6f +size 28235 diff --git a/sunnypilot/selfdrive/assets/offroad/icon_models.png b/sunnypilot/selfdrive/assets/offroad/icon_models.png new file mode 100644 index 0000000000..0131759703 --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_models.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9b431c274a55437b5a65ada7c85503e137bc2ffa8917510ad9affb14a407d82 +size 14366 diff --git a/sunnypilot/selfdrive/assets/offroad/icon_visuals.png b/sunnypilot/selfdrive/assets/offroad/icon_visuals.png new file mode 100644 index 0000000000..3cc9c3b145 --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_visuals.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ecaac6fb687fcab3300d1fe2dfea96fb49734d634a663e2c02ee0b26ebd773e +size 20632 diff --git a/sunnypilot/selfdrive/car/cruise_ext.py b/sunnypilot/selfdrive/car/cruise_ext.py new file mode 100644 index 0000000000..6f0419ed14 --- /dev/null +++ b/sunnypilot/selfdrive/car/cruise_ext.py @@ -0,0 +1,47 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import numpy as np + +from cereal import car +from openpilot.common.params import Params + +ButtonType = car.CarState.ButtonEvent.Type + + +class VCruiseHelperSP: + def __init__(self) -> None: + self.params = Params() + + self.custom_acc_enabled = self.params.get_bool("CustomAccIncrementsEnabled") + self.short_increment = self.read_int_param("CustomAccShortPressIncrement", 1) + self.long_increment = self.read_int_param("CustomAccLongPressIncrement", 5) + + def read_int_param(self, key: str, default: int = 0) -> int: + try: + return int(self.params.get(key, encoding='utf8')) + except (ValueError, TypeError): + return default + + def read_custom_set_speed_params(self) -> None: + self.custom_acc_enabled = self.params.get_bool("CustomAccIncrementsEnabled") + self.short_increment = self.read_int_param("CustomAccShortPressIncrement", 1) + self.long_increment = self.read_int_param("CustomAccLongPressIncrement", 5) + + def update_v_cruise_delta(self, long_press: bool, v_cruise_delta: float) -> tuple[bool, float]: + if not self.custom_acc_enabled: + v_cruise_delta = v_cruise_delta * (5 if long_press else 1) + return long_press, v_cruise_delta + + # Apply user-specified multipliers to the base increment + short_increment = np.clip(self.short_increment, 1, 10) + long_increment = np.clip(self.long_increment, 1, 10) + + actual_increment = long_increment if long_press else short_increment + round_to_nearest = actual_increment in (5, 10) + v_cruise_delta = v_cruise_delta * actual_increment + + return round_to_nearest, v_cruise_delta diff --git a/sunnypilot/selfdrive/car/tests/test_custom_cruise.py b/sunnypilot/selfdrive/car/tests/test_custom_cruise.py new file mode 100644 index 0000000000..2f9d976847 --- /dev/null +++ b/sunnypilot/selfdrive/car/tests/test_custom_cruise.py @@ -0,0 +1,149 @@ +import pytest +from parameterized import parameterized_class + +from cereal import car +from openpilot.common.conversions import Conversions as 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 + +ButtonEvent = car.CarState.ButtonEvent +ButtonType = car.CarState.ButtonEvent.Type + + +@parameterized_class(('pcm_cruise',), [(False,)]) +class TestCustomAccIncrements(TestVCruiseHelper): + def setup_method(self): + TestVCruiseHelper.setup_method(self) + self.params = Params() + self.reset_custom_params() + + def reset_custom_params(self) -> None: + """Reset to default custom ACC parameters""" + self.params.put_bool("CustomAccIncrementsEnabled", False) + self.params.put("CustomAccShortPressIncrement", "1") + self.params.put("CustomAccLongPressIncrement", "5") + self.v_cruise_helper.read_custom_set_speed_params() + + def press_button_short(self, button_type: car.CarState.ButtonEvent.Type) -> None: + """Simulate a short button press (press + release)""" + CS = car.CarState(cruiseState={"available": True}) + CS.buttonEvents = [ButtonEvent(type=button_type, pressed=True)] + self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=True) + + CS.buttonEvents = [ButtonEvent(type=button_type, pressed=False)] + self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=True) + + def press_button_long(self, button_type: car.CarState.ButtonEvent.Type) -> None: + """Simulate a long button press (50+ frames)""" + CS = car.CarState(cruiseState={"available": True}) + CS.buttonEvents = [ButtonEvent(type=button_type, pressed=True)] + self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=True) + + # Hold for 50 frames to trigger long press + CS.buttonEvents = [] + for _ in range(50): + self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=True) + + CS.buttonEvents = [ButtonEvent(type=button_type, pressed=False)] + self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=True) + + def set_custom_increments(self, enabled: bool, short_inc: int, long_inc: int) -> None: + """Set custom ACC increment parameters""" + self.params.put_bool("CustomAccIncrementsEnabled", enabled) + self.params.put("CustomAccShortPressIncrement", str(short_inc)) + self.params.put("CustomAccLongPressIncrement", str(long_inc)) + self.v_cruise_helper.read_custom_set_speed_params() + + def test_default_behavior_when_disabled(self): + """Test that default increments are used when custom ACC is disabled""" + self.set_custom_increments(enabled=False, short_inc=5, long_inc=10) + self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False) + + initial_speed = self.v_cruise_helper.v_cruise_kph + + # Short press should increment by 1 (default) + self.press_button_short(ButtonType.accelCruise) + assert self.v_cruise_helper.v_cruise_kph == initial_speed + 1 + + @pytest.mark.parametrize("increment", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) + def test_custom_short_press_increments(self, increment): + """Test custom short press increments (1-10)""" + self.set_custom_increments(enabled=True, short_inc=increment, long_inc=5) + self.enable(50 * CV.KPH_TO_MS, False, False) + + initial_speed = self.v_cruise_helper.v_cruise_kph + self.press_button_short(ButtonType.accelCruise) + + if increment in (5, 10): + # Should round to nearest increment + expected_speed = ((initial_speed // increment) + 1) * increment + else: + expected_speed = initial_speed + increment + + assert self.v_cruise_helper.v_cruise_kph == expected_speed + + @pytest.mark.parametrize("increment", (1, 5, 10)) + def test_custom_long_press_increments(self, increment): + """Test custom long press increments (1, 5, 10)""" + self.set_custom_increments(enabled=True, short_inc=1, long_inc=increment) + self.enable(50 * CV.KPH_TO_MS, False, False) + + initial_speed = self.v_cruise_helper.v_cruise_kph + self.press_button_long(ButtonType.accelCruise) + + if increment in (5, 10): + # Should round to nearest increment + expected_speed = ((initial_speed // increment) + 1) * increment + else: + expected_speed = initial_speed + increment + + assert self.v_cruise_helper.v_cruise_kph == expected_speed + + @pytest.mark.parametrize("button_type", [ButtonType.accelCruise, ButtonType.decelCruise]) + def test_accel_decel_symmetry(self, button_type): + """Test that acceleration and deceleration work symmetrically""" + self.set_custom_increments(enabled=True, short_inc=3, long_inc=5) + self.enable(50 * CV.KPH_TO_MS, False, False) + + initial_speed = self.v_cruise_helper.v_cruise_kph + self.press_button_short(button_type) + + expected_change = 3 if button_type == ButtonType.accelCruise else -3 + assert self.v_cruise_helper.v_cruise_kph == initial_speed + expected_change + + def test_rounding_behavior(self): + """Test rounding behavior for 5 and 10 increments""" + test_cases = [ + (47, 5, 50), # 47 -> 50 (round up to next 5) + (45, 5, 50), # 45 -> 50 (already at 5, increment by 5) + (43, 10, 50), # 43 -> 50 (round up to next 10) + (40, 10, 50), # 40 -> 50 (already at 10, increment by 10) + ] + + for initial, increment, expected in test_cases: + self.set_custom_increments(enabled=True, short_inc=increment, long_inc=increment) + self.reset_cruise_speed_state() + self.enable(initial * CV.KPH_TO_MS, False, False) + + self.press_button_short(ButtonType.accelCruise) + assert self.v_cruise_helper.v_cruise_kph == expected + + def test_invalid_values_fallback(self): + """Test that invalid values fallback to safe defaults""" + # Test invalid short increment + self.set_custom_increments(enabled=True, short_inc=-1, long_inc=5) + self.enable(50 * CV.KPH_TO_MS, False, False) + + initial_speed = self.v_cruise_helper.v_cruise_kph + self.press_button_short(ButtonType.accelCruise) + assert self.v_cruise_helper.v_cruise_kph == initial_speed + 1 # Should fallback to 1 + + # Test invalid long increment + self.reset_cruise_speed_state() + self.set_custom_increments(enabled=True, short_inc=1, long_inc=99) + self.enable(50 * CV.KPH_TO_MS, False, False) + + initial_speed = self.v_cruise_helper.v_cruise_kph + self.press_button_long(ButtonType.accelCruise) + assert self.v_cruise_helper.v_cruise_kph == initial_speed + 10 # Should fallback to 10 diff --git a/sunnypilot/selfdrive/controls/controlsd_ext.py b/sunnypilot/selfdrive/controls/controlsd_ext.py index 4978c01332..9a7f0cb5fc 100644 --- a/sunnypilot/selfdrive/controls/controlsd_ext.py +++ b/sunnypilot/selfdrive/controls/controlsd_ext.py @@ -11,12 +11,14 @@ from opendbc.car import structs from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.selfdrive.controls.lib.param_store import ParamStore +from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral class ControlsExt: def __init__(self, CP: structs.CarParams, params: Params): self.CP = CP self.params = params + self.blinker_pause_lateral = BlinkerPauseLateral() self.param_store = ParamStore(self.CP) self.get_params_sp() @@ -29,11 +31,13 @@ class ControlsExt: def get_params_sp(self) -> None: self.param_store.update(self.params) + self.blinker_pause_lateral.get_params() + + def get_lat_active(self, sm: messaging.SubMaster) -> bool: + if self.blinker_pause_lateral.update(sm['carState']): + return False - @staticmethod - def get_lat_active(sm: messaging.SubMaster) -> bool: ss_sp = sm['selfdriveStateSP'] - if ss_sp.mads.available: return bool(ss_sp.mads.active) diff --git a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py new file mode 100644 index 0000000000..fb86280358 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py @@ -0,0 +1,34 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +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.params import Params + + +class BlinkerPauseLateral: + def __init__(self): + self.params = Params() + + self.enabled = self.params.get_bool("BlinkerPauseLateralControl") + self.is_metric = self.params.get_bool("IsMetric") + self.min_speed = 0 + + def get_params(self) -> None: + self.enabled = self.params.get_bool("BlinkerPauseLateralControl") + self.is_metric = self.params.get_bool("IsMetric") + self.min_speed = int(self.params.get("BlinkerMinLateralControlSpeed", encoding='utf8')) + + def update(self, CS: car.CarState) -> bool: + if not self.enabled: + return False + + one_blinker = CS.leftBlinker != CS.rightBlinker + speed_factor = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS + min_speed_ms = self.min_speed * speed_factor + + return bool(one_blinker and CS.vEgo < min_speed_ms) diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py index 800bbac67a..8ccd81d8ff 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py @@ -11,7 +11,7 @@ from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.selfdrive.modeld.constants import ModelConstants LAT_PLAN_MIN_IDX = 5 - +LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan def get_predicted_lateral_jerk(lat_accels, t_diffs): # compute finite difference between subsequent model_v2.acceleration.y values @@ -85,12 +85,15 @@ class LatControlTorqueExtBase: # precompute time differences between ModelConstants.T_IDXS self.t_diffs = np.diff(ModelConstants.T_IDXS) - self.desired_lat_jerk_time = CP.steerActuatorDelay + 0.3 + self.desired_lat_jerk_time = CP.steerActuatorDelay + LATERAL_LAG_MOD def update_model_v2(self, model_v2): self.model_v2 = model_v2 self.model_valid = self.model_v2 is not None and len(self.model_v2.orientation.x) >= CONTROL_N + def update_lateral_lag(self, lag): + self.desired_lat_jerk_time = max(0.01, lag) + LATERAL_LAG_MOD + def update_friction_input(self, val_1, val_2): _error = val_1 - val_2 _value = self.lat_accel_friction_factor * _error + self.lat_jerk_friction_factor * self.lookahead_lateral_jerk diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index 218bbb9f6a..2f89594c8f 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -45,9 +45,8 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): self.pitch_last = 0.0 # setup future time offsets - self.nn_time_offset = CP.steerActuatorDelay + 0.2 - future_times = [0.3, 0.6, 1.0, 1.5] # seconds in the future - self.nn_future_times = [i + self.nn_time_offset for i in future_times] + self.future_times = [0.3, 0.6, 1.0, 1.5] # seconds in the future + self.nn_future_times = [i + self.desired_lat_jerk_time for i in self.future_times] # setup past time offsets self.past_times = [-0.3, -0.2, -0.1] @@ -58,6 +57,10 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): self.error_deque = deque(maxlen=history_check_frames[0]) self.past_future_len = len(self.past_times) + len(self.nn_future_times) + def update_lateral_lag(self, lag): + super().update_lateral_lag(lag) + self.nn_future_times = [t + self.desired_lat_jerk_time for t in self.future_times] + def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None: if not self.enabled or not self.model_valid or not self.has_nn_model: return diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py index 699327cd81..01ddec68ab 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py @@ -73,17 +73,21 @@ class TestNeuralNetworkLateralControl: controller.extension.model_v2 = model_v2 # Saturate for curvature limited and controller limited + test_lag = 0.3 for _ in range(1000): controller.extension.update_model_v2(model_v2) + controller.extension.update_lateral_lag(test_lag) _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True) assert lac_log.saturated for _ in range(1000): controller.extension.update_model_v2(model_v2) + controller.extension.update_lateral_lag(test_lag) _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False) assert not lac_log.saturated for _ in range(1000): controller.extension.update_model_v2(model_v2) + controller.extension.update_lateral_lag(test_lag) _, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False) assert lac_log.saturated diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py new file mode 100644 index 0000000000..6ba1dcef41 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py @@ -0,0 +1,130 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +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.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral + + +class TestBlinkerPauseLateral: + + def setup_method(self): + self.blinker_pause_lateral = BlinkerPauseLateral() + self._reset_states() + + def _reset_states(self): + self.blinker_pause_lateral.enabled = True + self.blinker_pause_lateral.is_metric = False + self.blinker_pause_lateral.min_speed = 20 # MPH + + self.CS = car.CarState.new_message() + self.CS.vEgo = 0 + self.CS.leftBlinker = False + self.CS.rightBlinker = False + + def _test_should_blinker_pause_lateral(self, expected_results) -> None: + for left in (True, False): + for right in (True, False): + self.CS.leftBlinker = left + self.CS.rightBlinker = right + + result = self.blinker_pause_lateral.update(self.CS) + assert result == expected_results[(left, right)] + + def test_below_min_speed_blinker(self): + self.CS.vEgo = 4.5 # ~10 MPH + + expected_results = { + (False, False): False, + (True, False): True, + (False, True): True, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_above_min_speed_blinker(self): + self.CS.vEgo = 13.4 # ~30 MPH + + expected_results = { + (False, False): False, + (True, False): False, + (False, True): False, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_just_below_min_speed(self): + self.CS.vEgo = (20 * CV.MPH_TO_MS) - 0.01 + + expected_results = { + (False, False): False, + (True, False): True, + (False, True): True, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_disabled(self): + self.blinker_pause_lateral.enabled = False + self.CS.vEgo = 4.5 # ~10 MPH + + expected_results = { + (False, False): False, + (True, False): False, + (False, True): False, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_metric_units_below_min_speed(self): + self.blinker_pause_lateral.is_metric = True + self.CS.vEgo = 5.0 # ~18 km/h + + expected_results = { + (False, False): False, + (True, False): True, + (False, True): True, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_metric_units_above_threshold(self): + self.blinker_pause_lateral.is_metric = True + self.CS.vEgo = 6.0 # ~21.6 km/h + + expected_results = { + (False, False): False, + (True, False): False, + (False, True): False, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + def test_change_min_speed_threshold(self): + self.blinker_pause_lateral.min_speed = 30 # MPH + + # below min speed + self.CS.vEgo = 11.2 # ~25 MPH + + expected_results = { + (False, False): False, + (True, False): True, + (False, True): True, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) + + # above min speed + self.CS.vEgo = 15.6 # ~35 MPH + + expected_results = { + (False, False): False, + (True, False): False, + (False, True): False, + (True, True): False + } + self._test_should_blinker_pause_lateral(expected_results) diff --git a/system/athena/registration.py b/system/athena/registration.py index d1e54181b8..fce984c03a 100755 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -7,6 +7,7 @@ from pathlib import Path from datetime import datetime, timedelta, UTC from openpilot.common.api import api_get from openpilot.common.params import Params +from openpilot.common.spinner import Spinner from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.system.hardware import HARDWARE, PC from openpilot.system.hardware.hw import Paths @@ -44,7 +45,6 @@ def register(show_spinner=False) -> str | None: cloudlog.warning(f"missing public key: {pubkey}") elif dongle_id is None: if show_spinner: - from openpilot.system.ui.spinner import Spinner spinner = Spinner() spinner.update("registering device") diff --git a/system/hardware/base.py b/system/hardware/base.py index e429f0e9f2..3506be5145 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -210,6 +210,9 @@ class HardwareBase(ABC): def configure_modem(self): pass + def reboot_modem(self): + pass + @abstractmethod def get_networks(self): pass diff --git a/system/hardware/esim.py b/system/hardware/esim.py index 6668a1cdd3..58ead6593f 100755 --- a/system/hardware/esim.py +++ b/system/hardware/esim.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import argparse - +import time from openpilot.system.hardware import HARDWARE @@ -14,14 +14,16 @@ if __name__ == '__main__': parser.add_argument('--nickname', nargs=2, metavar=('iccid', 'name'), help='update the nickname for a profile') args = parser.parse_args() + mutated = False lpa = HARDWARE.get_sim_lpa() if args.switch: lpa.switch_profile(args.switch) + mutated = True elif args.delete: confirm = input('are you sure you want to delete this profile? (y/N) ') if confirm == 'y': lpa.delete_profile(args.delete) - print('deleted profile, please restart device to apply changes') + mutated = True else: print('cancelled') exit(0) @@ -32,6 +34,11 @@ if __name__ == '__main__': else: parser.print_help() + if mutated: + HARDWARE.reboot_modem() + # eUICC needs a small delay post-reboot before querying profiles + time.sleep(.5) + profiles = lpa.list_profiles() print(f'\n{len(profiles)} profile{"s" if len(profiles) > 1 else ""}:') for p in profiles: diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index bb90c12ba2..223faeaafd 100755 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -34,6 +34,7 @@ CURRENT_TAU = 15. # 15s time constant TEMP_TAU = 5. # 5s time constant DISCONNECT_TIMEOUT = 5. # wait 5 seconds before going offroad after disconnect so you get an alert PANDA_STATES_TIMEOUT = round(1000 / SERVICE_LIST['pandaStates'].frequency * 1.5) # 1.5x the expected pandaState frequency +ONROAD_CYCLE_TIME = 1 # seconds to wait offroad after requesting an onroad cycle ThermalBand = namedtuple("ThermalBand", ['min_temp', 'max_temp']) HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'network_strength', 'network_stats', @@ -170,6 +171,7 @@ def hardware_thread(end_event, hw_queue) -> None: onroad_conditions: dict[str, bool] = { "ignition": False, + "not_onroad_cycle": True, } startup_conditions: dict[str, bool] = {} startup_conditions_prev: dict[str, bool] = {} @@ -195,6 +197,7 @@ def hardware_thread(end_event, hw_queue) -> None: should_start_prev = False in_car = False engaged_prev = False + offroad_cycle_count = 0 params = Params() power_monitor = PowerMonitoring() @@ -211,6 +214,12 @@ def hardware_thread(end_event, hw_queue) -> None: peripheralState = sm['peripheralState'] peripheral_panda_present = peripheralState.pandaType != log.PandaState.PandaType.unknown + # handle requests to cycle system started state + if params.get_bool("OnroadCycleRequested"): + params.put_bool("OnroadCycleRequested", False) + offroad_cycle_count = sm.frame + onroad_conditions["not_onroad_cycle"] = (sm.frame - offroad_cycle_count) >= ONROAD_CYCLE_TIME * SERVICE_LIST['pandaStates'].frequency + if sm.updated['pandaStates'] and len(pandaStates) > 0: # Set ignition based on any panda connected @@ -231,7 +240,7 @@ def hardware_thread(end_event, hw_queue) -> None: cloudlog.error("panda timed out onroad") # Run at 2Hz, plus either edge of ignition - ign_edge = (started_ts is not None) != onroad_conditions["ignition"] + ign_edge = (started_ts is not None) != all(onroad_conditions.values()) if (sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_HW) != 0) and not ign_edge: continue @@ -314,7 +323,6 @@ def hardware_thread(end_event, hw_queue) -> None: offroad_mode = params.get_bool("OffroadMode") startup_conditions["not_always_offroad"] = not offroad_mode onroad_conditions["not_always_offroad"] = not offroad_mode - set_offroad_alert("OffroadMode_Status", offroad_mode) # if the temperature enters the danger zone, go offroad to cool down onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger diff --git a/system/hardware/hw.py b/system/hardware/hw.py index 9722e35e30..5e40fff136 100644 --- a/system/hardware/hw.py +++ b/system/hardware/hw.py @@ -77,3 +77,10 @@ class Paths: return str(Path(Paths.comma_home()) / "community" / "crashes") else: return "/data/community/crashes" + + @staticmethod + def mapd_root() -> str: + if PC: + return str(Path(Paths.comma_home()) / "media" / "0" / "osm") + else: + return "/data/media/0/osm" diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index e209613f42..ec2557422c 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -56,28 +56,28 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-eb0dd0a8ffdc1c03aa28d055788d70be885a00f523b552d7acf79b954838e968.img.xz", - "hash": "eb0dd0a8ffdc1c03aa28d055788d70be885a00f523b552d7acf79b954838e968", - "hash_raw": "eb0dd0a8ffdc1c03aa28d055788d70be885a00f523b552d7acf79b954838e968", + "url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz", + "hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", + "hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", "size": 18479104, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "800868bd9d340f1fdf8340924caca374409624658324607621663fc5c7d10d4f" + "ondevice_hash": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-4c3fa88833a8a3876653fa53a658769ecc64be8fc6f6a28ce520ef79bf3061c1.img.xz", - "hash": "226794914e9d157b34cc86f1fbe1f2827a9a9919dbe560021b61573a7e5d3101", - "hash_raw": "4c3fa88833a8a3876653fa53a658769ecc64be8fc6f6a28ce520ef79bf3061c1", + "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz", + "hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164", + "hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", "size": 5368709120, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "236890f04ee21b7eb92a59bc47971bd96cad5a4381ed8935eb4be0cb5a4cf48b", + "ondevice_hash": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec", "alt": { - "hash": "4c3fa88833a8a3876653fa53a658769ecc64be8fc6f6a28ce520ef79bf3061c1", - "url": "https://commadist.azureedge.net/agnosupdate/system-4c3fa88833a8a3876653fa53a658769ecc64be8fc6f6a28ce520ef79bf3061c1.img", + "hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", + "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img", "size": 5368709120 } } diff --git a/system/hardware/tici/all-partitions.json b/system/hardware/tici/all-partitions.json index 7e39d275cc..d28b48128b 100644 --- a/system/hardware/tici/all-partitions.json +++ b/system/hardware/tici/all-partitions.json @@ -339,62 +339,62 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-eb0dd0a8ffdc1c03aa28d055788d70be885a00f523b552d7acf79b954838e968.img.xz", - "hash": "eb0dd0a8ffdc1c03aa28d055788d70be885a00f523b552d7acf79b954838e968", - "hash_raw": "eb0dd0a8ffdc1c03aa28d055788d70be885a00f523b552d7acf79b954838e968", + "url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz", + "hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", + "hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", "size": 18479104, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "800868bd9d340f1fdf8340924caca374409624658324607621663fc5c7d10d4f" + "ondevice_hash": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-4c3fa88833a8a3876653fa53a658769ecc64be8fc6f6a28ce520ef79bf3061c1.img.xz", - "hash": "226794914e9d157b34cc86f1fbe1f2827a9a9919dbe560021b61573a7e5d3101", - "hash_raw": "4c3fa88833a8a3876653fa53a658769ecc64be8fc6f6a28ce520ef79bf3061c1", + "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz", + "hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164", + "hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", "size": 5368709120, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "236890f04ee21b7eb92a59bc47971bd96cad5a4381ed8935eb4be0cb5a4cf48b", + "ondevice_hash": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec", "alt": { - "hash": "4c3fa88833a8a3876653fa53a658769ecc64be8fc6f6a28ce520ef79bf3061c1", - "url": "https://commadist.azureedge.net/agnosupdate/system-4c3fa88833a8a3876653fa53a658769ecc64be8fc6f6a28ce520ef79bf3061c1.img", + "hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", + "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img", "size": 5368709120 } }, { "name": "userdata_90", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-f6d24876234f6bea9cc753892eea99ac4b0c8646e93b93d76fc5dddce67347f1.img.xz", - "hash": "2485459e9fbfc0d88a59a017bfa193d97b80afed30d6f59db711e53f62a21c72", - "hash_raw": "f6d24876234f6bea9cc753892eea99ac4b0c8646e93b93d76fc5dddce67347f1", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21.img.xz", + "hash": "3d8a007bae088c5959eb9b82454013f91868946d78380fecea2b1afdfb575c02", + "hash_raw": "f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21", "size": 96636764160, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "893cfb3e95d6025b9fa6a5c8c3b97bf2abc4ff036c3da846a07536653dbb3a1d" + "ondevice_hash": "5bfbabb8ff96b149056aa75d5b7e66a7cdd9cb4bcefe23b922c292f7f3a43462" }, { "name": "userdata_89", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-14f0f02e53ce62a79b337d7b3174dcaffebaa4130264bb5ed9105d942338230b.img.xz", - "hash": "9222aebb280531b7015ee9bab3848dd195567d075b6f37f74d4cd0186685a11f", - "hash_raw": "14f0f02e53ce62a79b337d7b3174dcaffebaa4130264bb5ed9105d942338230b", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf.img.xz", + "hash": "443f136484294b210318842d09fb618d5411c8bdbab9f7421d8c89eb291a8d3f", + "hash_raw": "06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf", "size": 95563022336, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "a5459ec858d39e3d0709a09af430cb644fe640f92e0cd216b138f01401e4e17e" + "ondevice_hash": "67db02b29a7e4435951c64cc962a474d048ed444aa912f3494391417cd51a074" }, { "name": "userdata_30", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-180ad331538f20c00218d41591afed3a25bb0f76fa30b1a665cad102cf6c9f7d.img.xz", - "hash": "7fc09317f005676150bfcced43ebb1aa919d854c2511d547a588a35c1122bfe3", - "hash_raw": "180ad331538f20c00218d41591afed3a25bb0f76fa30b1a665cad102cf6c9f7d", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6.img.xz", + "hash": "875b580cb786f290a842e9187fd945657561886123eb3075a26f7995a18068f6", + "hash_raw": "06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6", "size": 32212254720, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "1a2f2d14c922bd2895073446460a0bd680711a7b14318a4402631635e6d133b3" + "ondevice_hash": "16e27ba3c5cf9f0394ce6235ba6021b8a2de293fdb08399f8ca832fa5e4d0b9d" } ] \ No newline at end of file diff --git a/system/hardware/tici/esim.py b/system/hardware/tici/esim.py index f657966ddf..b489286f50 100644 --- a/system/hardware/tici/esim.py +++ b/system/hardware/tici/esim.py @@ -54,15 +54,10 @@ class TiciLPA(LPABase): self._validate_successful(self._invoke('profile', 'nickname', iccid, nickname)) def switch_profile(self, iccid: str) -> None: - self._enable_profile(iccid) - - def _enable_profile(self, iccid: str) -> None: self._validate_profile_exists(iccid) latest = self.get_active_profile() - if latest: - if latest.iccid == iccid: - return - self._validate_successful(self._invoke('profile', 'disable', latest.iccid)) + if latest and latest.iccid == iccid: + return self._validate_successful(self._invoke('profile', 'enable', iccid)) self._process_notifications() diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 3add52d21c..8e4ba722c4 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -9,6 +9,7 @@ from functools import cached_property, lru_cache from pathlib import Path from cereal import log +from openpilot.common.util import sudo_read, sudo_write from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action from openpilot.system.hardware.base import HardwareBase, LPABase, ThermalConfig, ThermalZone from openpilot.system.hardware.tici import iwlist @@ -62,25 +63,6 @@ MM_MODEM_ACCESS_TECHNOLOGY_UMTS = 1 << 5 MM_MODEM_ACCESS_TECHNOLOGY_LTE = 1 << 14 -def sudo_write(val, path): - try: - with open(path, 'w') as f: - f.write(str(val)) - except PermissionError: - os.system(f"sudo chmod a+w {path}") - try: - with open(path, 'w') as f: - f.write(str(val)) - except PermissionError: - # fallback for debugfs files - os.system(f"sudo su -c 'echo {val} > {path}'") - -def sudo_read(path: str) -> str: - try: - return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() - except Exception: - return "" - def affine_irq(val, action): irqs = get_irqs_for_action(action) if len(irqs) == 0: diff --git a/system/manager/build.py b/system/manager/build.py index 771024794f..ae130f4b04 100755 --- a/system/manager/build.py +++ b/system/manager/build.py @@ -5,16 +5,16 @@ from pathlib import Path # NOTE: Do NOT import anything here that needs be built (e.g. params) from openpilot.common.basedir import BASEDIR +from openpilot.common.spinner import Spinner +from openpilot.common.text_window import TextWindow from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.system.hardware import HARDWARE, AGNOS -from openpilot.system.ui.spinner import Spinner -from openpilot.system.ui.text import TextWindow from openpilot.system.version import get_build_metadata MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9 CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache") -TOTAL_SCONS_NODES = 3130 +TOTAL_SCONS_NODES = 3800 MAX_BUILD_PROGRESS = 100 def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: @@ -88,7 +88,7 @@ def build(spinner: Spinner, dirty: bool = False, minimal: bool = False) -> None: if __name__ == "__main__": - with Spinner() as spinner: - spinner.update_progress(0, 100) - build_metadata = get_build_metadata() - build(spinner, build_metadata.openpilot.is_dirty, minimal = AGNOS) + spinner = Spinner() + spinner.update_progress(0, 100) + build_metadata = get_build_metadata() + build(spinner, build_metadata.openpilot.is_dirty, minimal = AGNOS) diff --git a/system/manager/manager.py b/system/manager/manager.py index 71f2bc525a..9de332df47 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -9,6 +9,7 @@ from cereal import log import cereal.messaging as messaging import openpilot.system.sentry as sentry from openpilot.common.params import Params, ParamKeyType +from openpilot.common.text_window import TextWindow from openpilot.system.hardware import HARDWARE from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog from openpilot.system.manager.process import ensure_running @@ -18,6 +19,8 @@ from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.system.version import get_build_metadata, terms_version, training_version from openpilot.system.hardware.hw import Paths +from openpilot.sunnypilot.mapd.mapd_installer import VERSION + def manager_init() -> None: save_bootlog() @@ -44,19 +47,33 @@ def manager_init() -> None: sunnypilot_default_params: list[tuple[str, str | bytes]] = [ ("AutoLaneChangeTimer", "0"), ("AutoLaneChangeBsmDelay", "0"), + ("BlindSpot", "0"), + ("BlinkerMinLateralControlSpeed", "20"), # MPH or km/h + ("BlinkerPauseLateralControl", "0"), + ("CustomAccIncrementsEnabled", "0"), + ("CustomAccLongPressIncrement", "5"), + ("CustomAccShortPressIncrement", "1"), + ("DeviceBootMode", "0"), ("DynamicExperimentalControl", "0"), ("HyundaiLongitudinalTuning", "0"), + ("LagdToggle", "1"), ("Mads", "1"), ("MadsMainCruiseAllowed", "1"), ("MadsSteeringMode", "0"), ("MadsUnifiedEngagementMode", "1"), + ("MapdVersion", f"{VERSION}"), ("MaxTimeOffroad", "1800"), + ("Brightness", "0"), ("ModelManager_LastSyncTime", "0"), ("ModelManager_ModelsCache", ""), ("NeuralNetworkLateralControl", "0"), ("QuietMode", "0"), ] + # device boot mode + if params.get("DeviceBootMode") == b"1": # start in always offroad mode + params.put_bool("OffroadMode", True) + if params.get_bool("RecordFrontLock"): params.put_bool("RecordFront", True) @@ -218,8 +235,6 @@ def main() -> None: if __name__ == "__main__": - from openpilot.system.ui.text import TextWindow - unblock_stdout() try: diff --git a/system/manager/process.py b/system/manager/process.py index 6c233c21ab..0ebddae049 100644 --- a/system/manager/process.py +++ b/system/manager/process.py @@ -16,9 +16,8 @@ import openpilot.system.sentry as sentry from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware.hw import Paths +from openpilot.common.watchdog import WATCHDOG_FN -WATCHDOG_FN = f"{Paths.shm_path()}/wd_" ENABLE_WATCHDOG = os.getenv("NO_WATCHDOG") is None @@ -96,7 +95,6 @@ class ManagerProcess(ABC): try: fn = WATCHDOG_FN + str(self.proc.pid) with open(fn, "rb") as f: - # TODO: why can't pylint find struct.unpack? self.last_watchdog_time = struct.unpack('Q', f.read())[0] except Exception: pass diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 0a572af132..2e3f754a76 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -1,10 +1,14 @@ import os import operator +import platform from cereal import car, custom from openpilot.common.params import Params from openpilot.system.hardware import PC, TICI from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess +from openpilot.system.hardware.hw import Paths + +from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH from sunnypilot.models.helpers import get_active_model_runner from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, use_sunnylink_uploader @@ -100,15 +104,15 @@ procs = [ NativeProcess("camerad", "system/camerad", ["./camerad"], driverview, enabled=not WEBCAM), PythonProcess("webcamerad", "tools.webcam.camerad", driverview, enabled=WEBCAM), - NativeProcess("logcatd", "system/logcatd", ["./logcatd"], only_onroad), - NativeProcess("proclogd", "system/proclogd", ["./proclogd"], only_onroad), + NativeProcess("logcatd", "system/logcatd", ["./logcatd"], only_onroad, platform.system() != "Darwin"), + NativeProcess("proclogd", "system/proclogd", ["./proclogd"], only_onroad, platform.system() != "Darwin"), PythonProcess("micd", "system.micd", iscar), PythonProcess("timed", "system.timed", always_run, enabled=not PC), PythonProcess("modeld", "selfdrive.modeld.modeld", and_(only_onroad, is_stock_model)), PythonProcess("dmonitoringmodeld", "selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(WEBCAM or not PC)), - NativeProcess("sensord", "system/sensord", ["./sensord"], only_onroad, enabled=not PC), + 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("soundd", "selfdrive.ui.soundd", only_onroad), PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad), @@ -156,6 +160,10 @@ procs += [ # Backup PythonProcess("backup_manager", "sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)), + + # mapd + NativeProcess("mapd", Paths.mapd_root(), [MAPD_PATH], always_run), + PythonProcess("mapd_manager", "sunnypilot.mapd.mapd_manager", always_run), ] if os.path.exists("./github_runner.sh"): diff --git a/system/sensord/.gitignore b/system/sensord/.gitignore deleted file mode 100644 index e17675e254..0000000000 --- a/system/sensord/.gitignore +++ /dev/null @@ -1 +0,0 @@ -sensord diff --git a/system/sensord/SConscript b/system/sensord/SConscript deleted file mode 100644 index e2dfb522c6..0000000000 --- a/system/sensord/SConscript +++ /dev/null @@ -1,17 +0,0 @@ -Import('env', 'arch', 'common', 'messaging') - -sensors = [ - 'sensors/i2c_sensor.cc', - 'sensors/bmx055_accel.cc', - 'sensors/bmx055_gyro.cc', - 'sensors/bmx055_magn.cc', - 'sensors/bmx055_temp.cc', - 'sensors/lsm6ds3_accel.cc', - 'sensors/lsm6ds3_gyro.cc', - 'sensors/lsm6ds3_temp.cc', - 'sensors/mmc5603nj_magn.cc', -] -libs = [common, messaging, 'pthread'] -if arch == "larch64": - libs.append('i2c') -env.Program('sensord', ['sensors_qcom2.cc'] + sensors, LIBS=libs) diff --git a/system/sensord/sensord.py b/system/sensord/sensord.py new file mode 100755 index 0000000000..ed9d0ed415 --- /dev/null +++ b/system/sensord/sensord.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +import os +import time +import ctypes +import select +import threading + +import cereal.messaging as messaging +from cereal.services import SERVICE_LIST +from openpilot.common.util import sudo_write +from openpilot.common.realtime import config_realtime_process, Ratekeeper +from openpilot.common.swaglog import cloudlog +from openpilot.common.gpio import gpiochip_get_ro_value_fd, gpioevent_data + +from openpilot.system.sensord.sensors.i2c_sensor import Sensor +from openpilot.system.sensord.sensors.lsm6ds3_accel import LSM6DS3_Accel +from openpilot.system.sensord.sensors.lsm6ds3_gyro import LSM6DS3_Gyro +from openpilot.system.sensord.sensors.lsm6ds3_temp import LSM6DS3_Temp +from openpilot.system.sensord.sensors.mmc5603nj_magn import MMC5603NJ_Magn + +I2C_BUS_IMU = 1 + +def interrupt_loop(sensors: list[tuple[Sensor, str, bool]], event) -> None: + pm = messaging.PubMaster([service for sensor, service, interrupt in sensors if interrupt]) + + # Requesting both edges as the data ready pulse from the lsm6ds sensor is + # very short (75us) and is mostly detected as falling edge instead of rising. + # So if it is detected as rising the following falling edge is skipped. + fd = gpiochip_get_ro_value_fd("sensord", 0, 84) + + # Configure IRQ affinity + irq_path = "/proc/irq/336/smp_affinity_list" + if not os.path.exists(irq_path): + irq_path = "/proc/irq/335/smp_affinity_list" + if os.path.exists(irq_path): + sudo_write('1\n', irq_path) + + offset = time.time_ns() - time.monotonic_ns() + + poller = select.poll() + poller.register(fd, select.POLLIN | select.POLLPRI) + while not event.is_set(): + events = poller.poll(100) + if not events: + cloudlog.error("poll timed out") + continue + if not (events[0][1] & (select.POLLIN | select.POLLPRI)): + cloudlog.error("no poll events set") + continue + + dat = os.read(fd, ctypes.sizeof(gpioevent_data)*16) + evd = gpioevent_data.from_buffer_copy(dat) + + cur_offset = time.time_ns() - time.monotonic_ns() + if abs(cur_offset - offset) > 10 * 1e6: # ms + cloudlog.warning(f"time jumped: {cur_offset} {offset}") + offset = cur_offset + continue + + ts = evd.timestamp - cur_offset + for sensor, service, interrupt in sensors: + if interrupt: + try: + evt = sensor.get_event(ts) + if not sensor.is_data_valid(): + continue + msg = messaging.new_message(service, valid=True) + setattr(msg, service, evt) + pm.send(service, msg) + except Sensor.DataNotReady: + pass + except Exception: + cloudlog.exception(f"Error processing {service}") + + +def polling_loop(sensor: Sensor, service: str, event: threading.Event) -> None: + pm = messaging.PubMaster([service]) + rk = Ratekeeper(SERVICE_LIST[service].frequency, print_delay_threshold=None) + while not event.is_set(): + try: + evt = sensor.get_event() + if not sensor.is_data_valid(): + continue + msg = messaging.new_message(service, valid=True) + setattr(msg, service, evt) + pm.send(service, msg) + except Exception: + cloudlog.exception(f"Error in {service} polling loop") + rk.keep_time() + +def main() -> None: + config_realtime_process([1, ], 1) + + sensors_cfg = [ + (LSM6DS3_Accel(I2C_BUS_IMU), "accelerometer", True), + (LSM6DS3_Gyro(I2C_BUS_IMU), "gyroscope", True), + (LSM6DS3_Temp(I2C_BUS_IMU), "temperatureSensor", False), + (MMC5603NJ_Magn(I2C_BUS_IMU), "magnetometer", False), + ] + + # Initialize sensors + exit_event = threading.Event() + threads = [ + threading.Thread(target=interrupt_loop, args=(sensors_cfg, exit_event), daemon=True) + ] + for sensor, service, interrupt in sensors_cfg: + try: + sensor.init() + if not interrupt: + # Start polling thread for sensors without interrupts + threads.append(threading.Thread( + target=polling_loop, + args=(sensor, service, exit_event), + daemon=True + )) + except Exception: + cloudlog.exception(f"Error initializing {service} sensor") + + try: + for t in threads: + t.start() + while any(t.is_alive() for t in threads): + time.sleep(1) + except KeyboardInterrupt: + pass + finally: + exit_event.set() + for t in threads: + if t.is_alive(): + t.join() + + for sensor, _, _ in sensors_cfg: + try: + sensor.shutdown() + except Exception: + cloudlog.exception("Error shutting down sensor") + +if __name__ == "__main__": + main() diff --git a/system/sensord/sensors/__init__.py b/system/sensord/sensors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/system/sensord/sensors/bmx055_accel.cc b/system/sensord/sensors/bmx055_accel.cc deleted file mode 100644 index bcc31e1d6c..0000000000 --- a/system/sensord/sensors/bmx055_accel.cc +++ /dev/null @@ -1,85 +0,0 @@ -#include "system/sensord/sensors/bmx055_accel.h" - -#include - -#include "common/swaglog.h" -#include "common/timing.h" -#include "common/util.h" - -BMX055_Accel::BMX055_Accel(I2CBus *bus) : I2CSensor(bus) {} - -int BMX055_Accel::init() { - int ret = verify_chip_id(BMX055_ACCEL_I2C_REG_ID, {BMX055_ACCEL_CHIP_ID}); - if (ret == -1) { - goto fail; - } - - ret = set_register(BMX055_ACCEL_I2C_REG_PMU, BMX055_ACCEL_NORMAL_MODE); - if (ret < 0) { - goto fail; - } - - // bmx055 accel has a 1.3ms wakeup time from deep suspend mode - util::sleep_for(10); - - // High bandwidth - // ret = set_register(BMX055_ACCEL_I2C_REG_HBW, BMX055_ACCEL_HBW_ENABLE); - // if (ret < 0) { - // goto fail; - // } - - // Low bandwidth - ret = set_register(BMX055_ACCEL_I2C_REG_HBW, BMX055_ACCEL_HBW_DISABLE); - if (ret < 0) { - goto fail; - } - - ret = set_register(BMX055_ACCEL_I2C_REG_BW, BMX055_ACCEL_BW_125HZ); - if (ret < 0) { - goto fail; - } - - enabled = true; - -fail: - return ret; -} - -int BMX055_Accel::shutdown() { - if (!enabled) return 0; - - // enter deep suspend mode (lowest power mode) - int ret = set_register(BMX055_ACCEL_I2C_REG_PMU, BMX055_ACCEL_DEEP_SUSPEND); - if (ret < 0) { - LOGE("Could not move BMX055 ACCEL in deep suspend mode!"); - } - - return ret; -} - -bool BMX055_Accel::get_event(MessageBuilder &msg, uint64_t ts) { - uint64_t start_time = nanos_since_boot(); - uint8_t buffer[6]; - int len = read_register(BMX055_ACCEL_I2C_REG_X_LSB, buffer, sizeof(buffer)); - assert(len == 6); - - // 12 bit = +-2g - float scale = 9.81 * 2.0f / (1 << 11); - float x = -read_12_bit(buffer[0], buffer[1]) * scale; - float y = -read_12_bit(buffer[2], buffer[3]) * scale; - float z = read_12_bit(buffer[4], buffer[5]) * scale; - - auto event = msg.initEvent().initAccelerometer2(); - event.setSource(cereal::SensorEventData::SensorSource::BMX055); - event.setVersion(1); - event.setSensor(SENSOR_ACCELEROMETER); - event.setType(SENSOR_TYPE_ACCELEROMETER); - event.setTimestamp(start_time); - - float xyz[] = {x, y, z}; - auto svec = event.initAcceleration(); - svec.setV(xyz); - svec.setStatus(true); - - return true; -} diff --git a/system/sensord/sensors/bmx055_accel.h b/system/sensord/sensors/bmx055_accel.h deleted file mode 100644 index 2cc316e992..0000000000 --- a/system/sensord/sensors/bmx055_accel.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include "system/sensord/sensors/i2c_sensor.h" - -// Address of the chip on the bus -#define BMX055_ACCEL_I2C_ADDR 0x18 - -// Registers of the chip -#define BMX055_ACCEL_I2C_REG_ID 0x00 -#define BMX055_ACCEL_I2C_REG_X_LSB 0x02 -#define BMX055_ACCEL_I2C_REG_TEMP 0x08 -#define BMX055_ACCEL_I2C_REG_BW 0x10 -#define BMX055_ACCEL_I2C_REG_PMU 0x11 -#define BMX055_ACCEL_I2C_REG_HBW 0x13 -#define BMX055_ACCEL_I2C_REG_FIFO 0x3F - -// Constants -#define BMX055_ACCEL_CHIP_ID 0xFA - -#define BMX055_ACCEL_HBW_ENABLE 0b10000000 -#define BMX055_ACCEL_HBW_DISABLE 0b00000000 -#define BMX055_ACCEL_DEEP_SUSPEND 0b00100000 -#define BMX055_ACCEL_NORMAL_MODE 0b00000000 - -#define BMX055_ACCEL_BW_7_81HZ 0b01000 -#define BMX055_ACCEL_BW_15_63HZ 0b01001 -#define BMX055_ACCEL_BW_31_25HZ 0b01010 -#define BMX055_ACCEL_BW_62_5HZ 0b01011 -#define BMX055_ACCEL_BW_125HZ 0b01100 -#define BMX055_ACCEL_BW_250HZ 0b01101 -#define BMX055_ACCEL_BW_500HZ 0b01110 -#define BMX055_ACCEL_BW_1000HZ 0b01111 - -class BMX055_Accel : public I2CSensor { - uint8_t get_device_address() {return BMX055_ACCEL_I2C_ADDR;} -public: - BMX055_Accel(I2CBus *bus); - int init(); - bool get_event(MessageBuilder &msg, uint64_t ts = 0); - int shutdown(); -}; diff --git a/system/sensord/sensors/bmx055_gyro.cc b/system/sensord/sensors/bmx055_gyro.cc deleted file mode 100644 index 0cc405f654..0000000000 --- a/system/sensord/sensors/bmx055_gyro.cc +++ /dev/null @@ -1,92 +0,0 @@ -#include "system/sensord/sensors/bmx055_gyro.h" - -#include -#include - -#include "common/swaglog.h" -#include "common/util.h" - -#define DEG2RAD(x) ((x) * M_PI / 180.0) - - -BMX055_Gyro::BMX055_Gyro(I2CBus *bus) : I2CSensor(bus) {} - -int BMX055_Gyro::init() { - int ret = verify_chip_id(BMX055_GYRO_I2C_REG_ID, {BMX055_GYRO_CHIP_ID}); - if (ret == -1) return -1; - - ret = set_register(BMX055_GYRO_I2C_REG_LPM1, BMX055_GYRO_NORMAL_MODE); - if (ret < 0) { - goto fail; - } - // bmx055 gyro has a 30ms wakeup time from deep suspend mode - util::sleep_for(50); - - // High bandwidth - // ret = set_register(BMX055_GYRO_I2C_REG_HBW, BMX055_GYRO_HBW_ENABLE); - // if (ret < 0) { - // goto fail; - // } - - // Low bandwidth - ret = set_register(BMX055_GYRO_I2C_REG_HBW, BMX055_GYRO_HBW_DISABLE); - if (ret < 0) { - goto fail; - } - - // 116 Hz filter - ret = set_register(BMX055_GYRO_I2C_REG_BW, BMX055_GYRO_BW_116HZ); - if (ret < 0) { - goto fail; - } - - // +- 125 deg/s range - ret = set_register(BMX055_GYRO_I2C_REG_RANGE, BMX055_GYRO_RANGE_125); - if (ret < 0) { - goto fail; - } - - enabled = true; - -fail: - return ret; -} - -int BMX055_Gyro::shutdown() { - if (!enabled) return 0; - - // enter deep suspend mode (lowest power mode) - int ret = set_register(BMX055_GYRO_I2C_REG_LPM1, BMX055_GYRO_DEEP_SUSPEND); - if (ret < 0) { - LOGE("Could not move BMX055 GYRO in deep suspend mode!"); - } - - return ret; -} - -bool BMX055_Gyro::get_event(MessageBuilder &msg, uint64_t ts) { - uint64_t start_time = nanos_since_boot(); - uint8_t buffer[6]; - int len = read_register(BMX055_GYRO_I2C_REG_RATE_X_LSB, buffer, sizeof(buffer)); - assert(len == 6); - - // 16 bit = +- 125 deg/s - float scale = 125.0f / (1 << 15); - float x = -DEG2RAD(read_16_bit(buffer[0], buffer[1]) * scale); - float y = -DEG2RAD(read_16_bit(buffer[2], buffer[3]) * scale); - float z = DEG2RAD(read_16_bit(buffer[4], buffer[5]) * scale); - - auto event = msg.initEvent().initGyroscope2(); - event.setSource(cereal::SensorEventData::SensorSource::BMX055); - event.setVersion(1); - event.setSensor(SENSOR_GYRO_UNCALIBRATED); - event.setType(SENSOR_TYPE_GYROSCOPE_UNCALIBRATED); - event.setTimestamp(start_time); - - float xyz[] = {x, y, z}; - auto svec = event.initGyroUncalibrated(); - svec.setV(xyz); - svec.setStatus(true); - - return true; -} diff --git a/system/sensord/sensors/bmx055_gyro.h b/system/sensord/sensors/bmx055_gyro.h deleted file mode 100644 index 7be3e56563..0000000000 --- a/system/sensord/sensors/bmx055_gyro.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include "system/sensord/sensors/i2c_sensor.h" - -// Address of the chip on the bus -#define BMX055_GYRO_I2C_ADDR 0x68 - -// Registers of the chip -#define BMX055_GYRO_I2C_REG_ID 0x00 -#define BMX055_GYRO_I2C_REG_RATE_X_LSB 0x02 -#define BMX055_GYRO_I2C_REG_RANGE 0x0F -#define BMX055_GYRO_I2C_REG_BW 0x10 -#define BMX055_GYRO_I2C_REG_LPM1 0x11 -#define BMX055_GYRO_I2C_REG_HBW 0x13 -#define BMX055_GYRO_I2C_REG_FIFO 0x3F - -// Constants -#define BMX055_GYRO_CHIP_ID 0x0F - -#define BMX055_GYRO_HBW_ENABLE 0b10000000 -#define BMX055_GYRO_HBW_DISABLE 0b00000000 -#define BMX055_GYRO_DEEP_SUSPEND 0b00100000 -#define BMX055_GYRO_NORMAL_MODE 0b00000000 - -#define BMX055_GYRO_RANGE_2000 0b000 -#define BMX055_GYRO_RANGE_1000 0b001 -#define BMX055_GYRO_RANGE_500 0b010 -#define BMX055_GYRO_RANGE_250 0b011 -#define BMX055_GYRO_RANGE_125 0b100 - -#define BMX055_GYRO_BW_116HZ 0b0010 - - -class BMX055_Gyro : public I2CSensor { - uint8_t get_device_address() {return BMX055_GYRO_I2C_ADDR;} -public: - BMX055_Gyro(I2CBus *bus); - int init(); - bool get_event(MessageBuilder &msg, uint64_t ts = 0); - int shutdown(); -}; diff --git a/system/sensord/sensors/bmx055_magn.cc b/system/sensord/sensors/bmx055_magn.cc deleted file mode 100644 index b498c5fe3d..0000000000 --- a/system/sensord/sensors/bmx055_magn.cc +++ /dev/null @@ -1,258 +0,0 @@ -#include "system/sensord/sensors/bmx055_magn.h" - -#include - -#include -#include -#include - -#include "common/swaglog.h" -#include "common/util.h" - -static int16_t compensate_x(trim_data_t trim_data, int16_t mag_data_x, uint16_t data_rhall) { - uint16_t process_comp_x0 = data_rhall; - int32_t process_comp_x1 = ((int32_t)trim_data.dig_xyz1) * 16384; - uint16_t process_comp_x2 = ((uint16_t)(process_comp_x1 / process_comp_x0)) - ((uint16_t)0x4000); - int16_t retval = ((int16_t)process_comp_x2); - int32_t process_comp_x3 = (((int32_t)retval) * ((int32_t)retval)); - int32_t process_comp_x4 = (((int32_t)trim_data.dig_xy2) * (process_comp_x3 / 128)); - int32_t process_comp_x5 = (int32_t)(((int16_t)trim_data.dig_xy1) * 128); - int32_t process_comp_x6 = ((int32_t)retval) * process_comp_x5; - int32_t process_comp_x7 = (((process_comp_x4 + process_comp_x6) / 512) + ((int32_t)0x100000)); - int32_t process_comp_x8 = ((int32_t)(((int16_t)trim_data.dig_x2) + ((int16_t)0xA0))); - int32_t process_comp_x9 = ((process_comp_x7 * process_comp_x8) / 4096); - int32_t process_comp_x10 = ((int32_t)mag_data_x) * process_comp_x9; - retval = ((int16_t)(process_comp_x10 / 8192)); - retval = (retval + (((int16_t)trim_data.dig_x1) * 8)) / 16; - - return retval; -} - -static int16_t compensate_y(trim_data_t trim_data, int16_t mag_data_y, uint16_t data_rhall) { - uint16_t process_comp_y0 = trim_data.dig_xyz1; - int32_t process_comp_y1 = (((int32_t)trim_data.dig_xyz1) * 16384) / process_comp_y0; - uint16_t process_comp_y2 = ((uint16_t)process_comp_y1) - ((uint16_t)0x4000); - int16_t retval = ((int16_t)process_comp_y2); - int32_t process_comp_y3 = ((int32_t) retval) * ((int32_t)retval); - int32_t process_comp_y4 = ((int32_t)trim_data.dig_xy2) * (process_comp_y3 / 128); - int32_t process_comp_y5 = ((int32_t)(((int16_t)trim_data.dig_xy1) * 128)); - int32_t process_comp_y6 = ((process_comp_y4 + (((int32_t)retval) * process_comp_y5)) / 512); - int32_t process_comp_y7 = ((int32_t)(((int16_t)trim_data.dig_y2) + ((int16_t)0xA0))); - int32_t process_comp_y8 = (((process_comp_y6 + ((int32_t)0x100000)) * process_comp_y7) / 4096); - int32_t process_comp_y9 = (((int32_t)mag_data_y) * process_comp_y8); - retval = (int16_t)(process_comp_y9 / 8192); - retval = (retval + (((int16_t)trim_data.dig_y1) * 8)) / 16; - - return retval; -} - -static int16_t compensate_z(trim_data_t trim_data, int16_t mag_data_z, uint16_t data_rhall) { - int16_t process_comp_z0 = ((int16_t)data_rhall) - ((int16_t) trim_data.dig_xyz1); - int32_t process_comp_z1 = (((int32_t)trim_data.dig_z3) * ((int32_t)(process_comp_z0))) / 4; - int32_t process_comp_z2 = (((int32_t)(mag_data_z - trim_data.dig_z4)) * 32768); - int32_t process_comp_z3 = ((int32_t)trim_data.dig_z1) * (((int16_t)data_rhall) * 2); - int16_t process_comp_z4 = (int16_t)((process_comp_z3 + (32768)) / 65536); - int32_t retval = ((process_comp_z2 - process_comp_z1) / (trim_data.dig_z2 + process_comp_z4)); - - /* saturate result to +/- 2 micro-tesla */ - retval = std::clamp(retval, -32767, 32767); - - /* Conversion of LSB to micro-tesla*/ - retval = retval / 16; - - return (int16_t)retval; -} - -BMX055_Magn::BMX055_Magn(I2CBus *bus) : I2CSensor(bus) {} - -int BMX055_Magn::init() { - uint8_t trim_x1y1[2] = {0}; - uint8_t trim_x2y2[2] = {0}; - uint8_t trim_xy1xy2[2] = {0}; - uint8_t trim_z1[2] = {0}; - uint8_t trim_z2[2] = {0}; - uint8_t trim_z3[2] = {0}; - uint8_t trim_z4[2] = {0}; - uint8_t trim_xyz1[2] = {0}; - - // suspend -> sleep - int ret = set_register(BMX055_MAGN_I2C_REG_PWR_0, 0x01); - if (ret < 0) { - LOGD("Enabling power failed: %d", ret); - goto fail; - } - util::sleep_for(5); // wait until the chip is powered on - - ret = verify_chip_id(BMX055_MAGN_I2C_REG_ID, {BMX055_MAGN_CHIP_ID}); - if (ret == -1) { - goto fail; - } - - // Load magnetometer trim - ret = read_register(BMX055_MAGN_I2C_REG_DIG_X1, trim_x1y1, 2); - if (ret < 0) goto fail; - ret = read_register(BMX055_MAGN_I2C_REG_DIG_X2, trim_x2y2, 2); - if (ret < 0) goto fail; - ret = read_register(BMX055_MAGN_I2C_REG_DIG_XY2, trim_xy1xy2, 2); - if (ret < 0) goto fail; - ret = read_register(BMX055_MAGN_I2C_REG_DIG_Z1_LSB, trim_z1, 2); - if (ret < 0) goto fail; - ret = read_register(BMX055_MAGN_I2C_REG_DIG_Z2_LSB, trim_z2, 2); - if (ret < 0) goto fail; - ret = read_register(BMX055_MAGN_I2C_REG_DIG_Z3_LSB, trim_z3, 2); - if (ret < 0) goto fail; - ret = read_register(BMX055_MAGN_I2C_REG_DIG_Z4_LSB, trim_z4, 2); - if (ret < 0) goto fail; - ret = read_register(BMX055_MAGN_I2C_REG_DIG_XYZ1_LSB, trim_xyz1, 2); - if (ret < 0) goto fail; - - // Read trim data - trim_data.dig_x1 = trim_x1y1[0]; - trim_data.dig_y1 = trim_x1y1[1]; - - trim_data.dig_x2 = trim_x2y2[0]; - trim_data.dig_y2 = trim_x2y2[1]; - - trim_data.dig_xy1 = trim_xy1xy2[1]; // NB: MSB/LSB swapped - trim_data.dig_xy2 = trim_xy1xy2[0]; - - trim_data.dig_z1 = read_16_bit(trim_z1[0], trim_z1[1]); - trim_data.dig_z2 = read_16_bit(trim_z2[0], trim_z2[1]); - trim_data.dig_z3 = read_16_bit(trim_z3[0], trim_z3[1]); - trim_data.dig_z4 = read_16_bit(trim_z4[0], trim_z4[1]); - - trim_data.dig_xyz1 = read_16_bit(trim_xyz1[0], trim_xyz1[1] & 0x7f); - assert(trim_data.dig_xyz1 != 0); - - perform_self_test(); - - // f_max = 1 / (145us * nXY + 500us * NZ + 980us) - // Chose NXY = 7, NZ = 12, which gives 125 Hz, - // and has the same ratio as the high accuracy preset - ret = set_register(BMX055_MAGN_I2C_REG_REPXY, (7 - 1) / 2); - if (ret < 0) { - goto fail; - } - - ret = set_register(BMX055_MAGN_I2C_REG_REPZ, 12 - 1); - if (ret < 0) { - goto fail; - } - - enabled = true; - return 0; - - fail: - return ret; -} - -int BMX055_Magn::shutdown() { - if (!enabled) return 0; - - // move to suspend mode - int ret = set_register(BMX055_MAGN_I2C_REG_PWR_0, 0); - if (ret < 0) { - LOGE("Could not move BMX055 MAGN in suspend mode!"); - } - - return ret; -} - -bool BMX055_Magn::perform_self_test() { - uint8_t buffer[8]; - int16_t x, y; - int16_t neg_z, pos_z; - - // Increase z reps for less false positives (~30 Hz ODR) - set_register(BMX055_MAGN_I2C_REG_REPXY, 1); - set_register(BMX055_MAGN_I2C_REG_REPZ, 64 - 1); - - // Clean existing measurement - read_register(BMX055_MAGN_I2C_REG_DATAX_LSB, buffer, sizeof(buffer)); - - uint8_t forced = BMX055_MAGN_FORCED; - - // Negative current - set_register(BMX055_MAGN_I2C_REG_MAG, forced | (uint8_t(0b10) << 6)); - util::sleep_for(100); - - read_register(BMX055_MAGN_I2C_REG_DATAX_LSB, buffer, sizeof(buffer)); - parse_xyz(buffer, &x, &y, &neg_z); - - // Positive current - set_register(BMX055_MAGN_I2C_REG_MAG, forced | (uint8_t(0b11) << 6)); - util::sleep_for(100); - - read_register(BMX055_MAGN_I2C_REG_DATAX_LSB, buffer, sizeof(buffer)); - parse_xyz(buffer, &x, &y, &pos_z); - - // Put back in normal mode - set_register(BMX055_MAGN_I2C_REG_MAG, 0); - - int16_t diff = pos_z - neg_z; - bool passed = (diff > 180) && (diff < 240); - - if (!passed) { - LOGE("self test failed: neg %d pos %d diff %d", neg_z, pos_z, diff); - } - - return passed; -} - -bool BMX055_Magn::parse_xyz(uint8_t buffer[8], int16_t *x, int16_t *y, int16_t *z) { - bool ready = buffer[6] & 0x1; - if (ready) { - int16_t mdata_x = (int16_t) (((int16_t)buffer[1] << 8) | buffer[0]) >> 3; - int16_t mdata_y = (int16_t) (((int16_t)buffer[3] << 8) | buffer[2]) >> 3; - int16_t mdata_z = (int16_t) (((int16_t)buffer[5] << 8) | buffer[4]) >> 1; - uint16_t data_r = (uint16_t) (((uint16_t)buffer[7] << 8) | buffer[6]) >> 2; - assert(data_r != 0); - - *x = compensate_x(trim_data, mdata_x, data_r); - *y = compensate_y(trim_data, mdata_y, data_r); - *z = compensate_z(trim_data, mdata_z, data_r); - } - return ready; -} - - -bool BMX055_Magn::get_event(MessageBuilder &msg, uint64_t ts) { - uint64_t start_time = nanos_since_boot(); - uint8_t buffer[8]; - int16_t _x, _y, x, y, z; - - int len = read_register(BMX055_MAGN_I2C_REG_DATAX_LSB, buffer, sizeof(buffer)); - assert(len == sizeof(buffer)); - - bool parsed = parse_xyz(buffer, &_x, &_y, &z); - if (parsed) { - - auto event = msg.initEvent().initMagnetometer(); - event.setSource(cereal::SensorEventData::SensorSource::BMX055); - event.setVersion(2); - event.setSensor(SENSOR_MAGNETOMETER_UNCALIBRATED); - event.setType(SENSOR_TYPE_MAGNETIC_FIELD_UNCALIBRATED); - event.setTimestamp(start_time); - - // Move magnetometer into same reference frame as accel/gryo - x = -_y; - y = _x; - - // Axis convention - x = -x; - y = -y; - - float xyz[] = {(float)x, (float)y, (float)z}; - auto svec = event.initMagneticUncalibrated(); - svec.setV(xyz); - svec.setStatus(true); - } - - // The BMX055 Magnetometer has no FIFO mode. Self running mode only goes - // up to 30 Hz. Therefore we put in forced mode, and request measurements - // at a 100 Hz. When reading the registers we have to check the ready bit - // To verify the measurement was completed this cycle. - set_register(BMX055_MAGN_I2C_REG_MAG, BMX055_MAGN_FORCED); - - return parsed; -} diff --git a/system/sensord/sensors/bmx055_magn.h b/system/sensord/sensors/bmx055_magn.h deleted file mode 100644 index 15c4e734b9..0000000000 --- a/system/sensord/sensors/bmx055_magn.h +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once -#include - -#include "system/sensord/sensors/i2c_sensor.h" - -// Address of the chip on the bus -#define BMX055_MAGN_I2C_ADDR 0x10 - -// Registers of the chip -#define BMX055_MAGN_I2C_REG_ID 0x40 -#define BMX055_MAGN_I2C_REG_PWR_0 0x4B -#define BMX055_MAGN_I2C_REG_MAG 0x4C -#define BMX055_MAGN_I2C_REG_DATAX_LSB 0x42 -#define BMX055_MAGN_I2C_REG_RHALL_LSB 0x48 -#define BMX055_MAGN_I2C_REG_REPXY 0x51 -#define BMX055_MAGN_I2C_REG_REPZ 0x52 - -#define BMX055_MAGN_I2C_REG_DIG_X1 0x5D -#define BMX055_MAGN_I2C_REG_DIG_Y1 0x5E -#define BMX055_MAGN_I2C_REG_DIG_Z4_LSB 0x62 -#define BMX055_MAGN_I2C_REG_DIG_Z4_MSB 0x63 -#define BMX055_MAGN_I2C_REG_DIG_X2 0x64 -#define BMX055_MAGN_I2C_REG_DIG_Y2 0x65 -#define BMX055_MAGN_I2C_REG_DIG_Z2_LSB 0x68 -#define BMX055_MAGN_I2C_REG_DIG_Z2_MSB 0x69 -#define BMX055_MAGN_I2C_REG_DIG_Z1_LSB 0x6A -#define BMX055_MAGN_I2C_REG_DIG_Z1_MSB 0x6B -#define BMX055_MAGN_I2C_REG_DIG_XYZ1_LSB 0x6C -#define BMX055_MAGN_I2C_REG_DIG_XYZ1_MSB 0x6D -#define BMX055_MAGN_I2C_REG_DIG_Z3_LSB 0x6E -#define BMX055_MAGN_I2C_REG_DIG_Z3_MSB 0x6F -#define BMX055_MAGN_I2C_REG_DIG_XY2 0x70 -#define BMX055_MAGN_I2C_REG_DIG_XY1 0x71 - -// Constants -#define BMX055_MAGN_CHIP_ID 0x32 -#define BMX055_MAGN_FORCED (0b01 << 1) - -struct trim_data_t { - int8_t dig_x1; - int8_t dig_y1; - int8_t dig_x2; - int8_t dig_y2; - uint16_t dig_z1; - int16_t dig_z2; - int16_t dig_z3; - int16_t dig_z4; - uint8_t dig_xy1; - int8_t dig_xy2; - uint16_t dig_xyz1; -}; - - -class BMX055_Magn : public I2CSensor{ - uint8_t get_device_address() {return BMX055_MAGN_I2C_ADDR;} - trim_data_t trim_data = {0}; - bool perform_self_test(); - bool parse_xyz(uint8_t buffer[8], int16_t *x, int16_t *y, int16_t *z); -public: - BMX055_Magn(I2CBus *bus); - int init(); - bool get_event(MessageBuilder &msg, uint64_t ts = 0); - int shutdown(); -}; diff --git a/system/sensord/sensors/bmx055_temp.cc b/system/sensord/sensors/bmx055_temp.cc deleted file mode 100644 index da7b86476c..0000000000 --- a/system/sensord/sensors/bmx055_temp.cc +++ /dev/null @@ -1,31 +0,0 @@ -#include "system/sensord/sensors/bmx055_temp.h" - -#include - -#include "system/sensord/sensors/bmx055_accel.h" -#include "common/swaglog.h" -#include "common/timing.h" - -BMX055_Temp::BMX055_Temp(I2CBus *bus) : I2CSensor(bus) {} - -int BMX055_Temp::init() { - return verify_chip_id(BMX055_ACCEL_I2C_REG_ID, {BMX055_ACCEL_CHIP_ID}) == -1 ? -1 : 0; -} - -bool BMX055_Temp::get_event(MessageBuilder &msg, uint64_t ts) { - uint64_t start_time = nanos_since_boot(); - uint8_t buffer[1]; - int len = read_register(BMX055_ACCEL_I2C_REG_TEMP, buffer, sizeof(buffer)); - assert(len == sizeof(buffer)); - - float temp = 23.0f + int8_t(buffer[0]) / 2.0f; - - auto event = msg.initEvent().initTemperatureSensor(); - event.setSource(cereal::SensorEventData::SensorSource::BMX055); - event.setVersion(1); - event.setType(SENSOR_TYPE_AMBIENT_TEMPERATURE); - event.setTimestamp(start_time); - event.setTemperature(temp); - - return true; -} diff --git a/system/sensord/sensors/bmx055_temp.h b/system/sensord/sensors/bmx055_temp.h deleted file mode 100644 index a2eabae395..0000000000 --- a/system/sensord/sensors/bmx055_temp.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include "system/sensord/sensors/bmx055_accel.h" -#include "system/sensord/sensors/i2c_sensor.h" - -class BMX055_Temp : public I2CSensor { - uint8_t get_device_address() {return BMX055_ACCEL_I2C_ADDR;} -public: - BMX055_Temp(I2CBus *bus); - int init(); - bool get_event(MessageBuilder &msg, uint64_t ts = 0); - int shutdown() { return 0; } -}; diff --git a/system/sensord/sensors/constants.h b/system/sensord/sensors/constants.h deleted file mode 100644 index c216f838a5..0000000000 --- a/system/sensord/sensors/constants.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - - -#define SENSOR_ACCELEROMETER 1 -#define SENSOR_MAGNETOMETER 2 -#define SENSOR_MAGNETOMETER_UNCALIBRATED 3 -#define SENSOR_GYRO 4 -#define SENSOR_GYRO_UNCALIBRATED 5 -#define SENSOR_LIGHT 7 - -#define SENSOR_TYPE_ACCELEROMETER 1 -#define SENSOR_TYPE_GEOMAGNETIC_FIELD 2 -#define SENSOR_TYPE_GYROSCOPE 4 -#define SENSOR_TYPE_LIGHT 5 -#define SENSOR_TYPE_AMBIENT_TEMPERATURE 13 -#define SENSOR_TYPE_MAGNETIC_FIELD_UNCALIBRATED 14 -#define SENSOR_TYPE_MAGNETIC_FIELD SENSOR_TYPE_GEOMAGNETIC_FIELD -#define SENSOR_TYPE_GYROSCOPE_UNCALIBRATED 16 diff --git a/system/sensord/sensors/i2c_sensor.cc b/system/sensord/sensors/i2c_sensor.cc deleted file mode 100644 index 90220f551d..0000000000 --- a/system/sensord/sensors/i2c_sensor.cc +++ /dev/null @@ -1,50 +0,0 @@ -#include "system/sensord/sensors/i2c_sensor.h" - -int16_t read_12_bit(uint8_t lsb, uint8_t msb) { - uint16_t combined = (uint16_t(msb) << 8) | uint16_t(lsb & 0xF0); - return int16_t(combined) / (1 << 4); -} - -int16_t read_16_bit(uint8_t lsb, uint8_t msb) { - uint16_t combined = (uint16_t(msb) << 8) | uint16_t(lsb); - return int16_t(combined); -} - -int32_t read_20_bit(uint8_t b2, uint8_t b1, uint8_t b0) { - uint32_t combined = (uint32_t(b0) << 16) | (uint32_t(b1) << 8) | uint32_t(b2); - return int32_t(combined) / (1 << 4); -} - -I2CSensor::I2CSensor(I2CBus *bus, int gpio_nr, bool shared_gpio) : - bus(bus), gpio_nr(gpio_nr), shared_gpio(shared_gpio) {} - -I2CSensor::~I2CSensor() { - if (gpio_fd != -1) { - close(gpio_fd); - } -} - -int I2CSensor::read_register(uint register_address, uint8_t *buffer, uint8_t len) { - return bus->read_register(get_device_address(), register_address, buffer, len); -} - -int I2CSensor::set_register(uint register_address, uint8_t data) { - return bus->set_register(get_device_address(), register_address, data); -} - -int I2CSensor::init_gpio() { - if (shared_gpio || gpio_nr == 0) { - return 0; - } - - gpio_fd = gpiochip_get_ro_value_fd("sensord", GPIOCHIP_INT, gpio_nr); - if (gpio_fd < 0) { - return -1; - } - - return 0; -} - -bool I2CSensor::has_interrupt_enabled() { - return gpio_nr != 0; -} diff --git a/system/sensord/sensors/i2c_sensor.h b/system/sensord/sensors/i2c_sensor.h deleted file mode 100644 index e6d328ce72..0000000000 --- a/system/sensord/sensors/i2c_sensor.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include -#include -#include -#include "cereal/gen/cpp/log.capnp.h" - -#include "common/i2c.h" -#include "common/gpio.h" - -#include "common/swaglog.h" -#include "system/sensord/sensors/constants.h" -#include "system/sensord/sensors/sensor.h" - -int16_t read_12_bit(uint8_t lsb, uint8_t msb); -int16_t read_16_bit(uint8_t lsb, uint8_t msb); -int32_t read_20_bit(uint8_t b2, uint8_t b1, uint8_t b0); - - -class I2CSensor : public Sensor { -private: - I2CBus *bus; - int gpio_nr; - bool shared_gpio; - virtual uint8_t get_device_address() = 0; - -public: - I2CSensor(I2CBus *bus, int gpio_nr = 0, bool shared_gpio = false); - ~I2CSensor(); - int read_register(uint register_address, uint8_t *buffer, uint8_t len); - int set_register(uint register_address, uint8_t data); - int init_gpio(); - bool has_interrupt_enabled(); - virtual int init() = 0; - virtual bool get_event(MessageBuilder &msg, uint64_t ts = 0) = 0; - virtual int shutdown() = 0; - - int verify_chip_id(uint8_t address, const std::vector &expected_ids) { - uint8_t chip_id = 0; - int ret = read_register(address, &chip_id, 1); - if (ret < 0) { - LOGD("Reading chip ID failed: %d", ret); - return -1; - } - for (int i = 0; i < expected_ids.size(); ++i) { - if (chip_id == expected_ids[i]) return chip_id; - } - LOGE("Chip ID wrong. Got: %d, Expected %d", chip_id, expected_ids[0]); - return -1; - } -}; diff --git a/system/sensord/sensors/i2c_sensor.py b/system/sensord/sensors/i2c_sensor.py new file mode 100644 index 0000000000..0e15a6622b --- /dev/null +++ b/system/sensord/sensors/i2c_sensor.py @@ -0,0 +1,72 @@ +import time +import smbus2 +import ctypes +from collections.abc import Iterable + +from cereal import log + +class Sensor: + class SensorException(Exception): + pass + + class DataNotReady(SensorException): + pass + + def __init__(self, bus: int) -> None: + self.bus = smbus2.SMBus(bus) + self.source = log.SensorEventData.SensorSource.velodyne # unknown + self.start_ts = 0. + + def __del__(self): + self.bus.close() + + def read(self, addr: int, length: int) -> bytes: + return bytes(self.bus.read_i2c_block_data(self.device_address, addr, length)) + + def write(self, addr: int, data: int) -> None: + self.bus.write_byte_data(self.device_address, addr, data) + + def writes(self, writes: Iterable[tuple[int, int]]) -> None: + for addr, data in writes: + self.write(addr, data) + + def verify_chip_id(self, address: int, expected_ids: list[int]) -> int: + chip_id = self.read(address, 1)[0] + assert chip_id in expected_ids + return chip_id + + # Abstract methods that must be implemented by subclasses + @property + def device_address(self) -> int: + raise NotImplementedError + + def init(self) -> None: + raise NotImplementedError + + def get_event(self, ts: int | None = None) -> log.SensorEventData: + raise NotImplementedError + + def shutdown(self) -> None: + raise NotImplementedError + + def is_data_valid(self) -> bool: + if self.start_ts == 0: + self.start_ts = time.monotonic() + + # unclear whether we need this... + return (time.monotonic() - self.start_ts) > 0.5 + + # *** helpers *** + @staticmethod + def wait(): + # a standard small sleep + time.sleep(0.005) + + @staticmethod + def parse_16bit(lsb: int, msb: int) -> int: + return ctypes.c_int16((msb << 8) | lsb).value + + @staticmethod + def parse_20bit(b2: int, b1: int, b0: int) -> int: + combined = ctypes.c_uint32((b0 << 16) | (b1 << 8) | b2).value + return ctypes.c_int32(combined).value // (1 << 4) diff --git a/system/sensord/sensors/lsm6ds3_accel.cc b/system/sensord/sensors/lsm6ds3_accel.cc deleted file mode 100644 index 03533e0657..0000000000 --- a/system/sensord/sensors/lsm6ds3_accel.cc +++ /dev/null @@ -1,250 +0,0 @@ -#include "system/sensord/sensors/lsm6ds3_accel.h" - -#include -#include -#include - -#include "common/swaglog.h" -#include "common/timing.h" -#include "common/util.h" - -LSM6DS3_Accel::LSM6DS3_Accel(I2CBus *bus, int gpio_nr, bool shared_gpio) : - I2CSensor(bus, gpio_nr, shared_gpio) {} - -void LSM6DS3_Accel::wait_for_data_ready() { - uint8_t drdy = 0; - uint8_t buffer[6]; - - do { - read_register(LSM6DS3_ACCEL_I2C_REG_STAT_REG, &drdy, sizeof(drdy)); - drdy &= LSM6DS3_ACCEL_DRDY_XLDA; - } while (drdy == 0); - - read_register(LSM6DS3_ACCEL_I2C_REG_OUTX_L_XL, buffer, sizeof(buffer)); -} - -void LSM6DS3_Accel::read_and_avg_data(float* out_buf) { - uint8_t drdy = 0; - uint8_t buffer[6]; - - float scaling = 0.061f; - if (source == cereal::SensorEventData::SensorSource::LSM6DS3TRC) { - scaling = 0.122f; - } - - for (int i = 0; i < 5; i++) { - do { - read_register(LSM6DS3_ACCEL_I2C_REG_STAT_REG, &drdy, sizeof(drdy)); - drdy &= LSM6DS3_ACCEL_DRDY_XLDA; - } while (drdy == 0); - - int len = read_register(LSM6DS3_ACCEL_I2C_REG_OUTX_L_XL, buffer, sizeof(buffer)); - assert(len == sizeof(buffer)); - - for (int j = 0; j < 3; j++) { - out_buf[j] += (float)read_16_bit(buffer[j*2], buffer[j*2+1]) * scaling; - } - } - - for (int i = 0; i < 3; i++) { - out_buf[i] /= 5.0f; - } -} - -int LSM6DS3_Accel::self_test(int test_type) { - float val_st_off[3] = {0}; - float val_st_on[3] = {0}; - float test_val[3] = {0}; - uint8_t ODR_FS_MO = LSM6DS3_ACCEL_ODR_52HZ; // full scale: +-2g, ODR: 52Hz - - // prepare sensor for self-test - - // enable block data update and automatic increment - int ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL3_C, LSM6DS3_ACCEL_IF_INC_BDU); - if (ret < 0) { - return ret; - } - - if (source == cereal::SensorEventData::SensorSource::LSM6DS3TRC) { - ODR_FS_MO = LSM6DS3_ACCEL_FS_4G | LSM6DS3_ACCEL_ODR_52HZ; - } - ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, ODR_FS_MO); - if (ret < 0) { - return ret; - } - - // wait for stable output, and discard first values - util::sleep_for(100); - wait_for_data_ready(); - read_and_avg_data(val_st_off); - - // enable Self Test positive (or negative) - ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL5_C, test_type); - if (ret < 0) { - return ret; - } - - // wait for stable output, and discard first values - util::sleep_for(100); - wait_for_data_ready(); - read_and_avg_data(val_st_on); - - // disable sensor - ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, 0); - if (ret < 0) { - return ret; - } - - // disable self test - ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL5_C, 0); - if (ret < 0) { - return ret; - } - - // calculate the mg values for self test - for (int i = 0; i < 3; i++) { - test_val[i] = fabs(val_st_on[i] - val_st_off[i]); - } - - // verify test result - for (int i = 0; i < 3; i++) { - if ((LSM6DS3_ACCEL_MIN_ST_LIMIT_mg > test_val[i]) || - (test_val[i] > LSM6DS3_ACCEL_MAX_ST_LIMIT_mg)) { - return -1; - } - } - - return ret; -} - -int LSM6DS3_Accel::init() { - uint8_t value = 0; - bool do_self_test = false; - - const char* env_lsm_selftest = std::getenv("LSM_SELF_TEST"); - if (env_lsm_selftest != nullptr && strncmp(env_lsm_selftest, "1", 1) == 0) { - do_self_test = true; - } - - int ret = verify_chip_id(LSM6DS3_ACCEL_I2C_REG_ID, {LSM6DS3_ACCEL_CHIP_ID, LSM6DS3TRC_ACCEL_CHIP_ID}); - if (ret == -1) return -1; - - if (ret == LSM6DS3TRC_ACCEL_CHIP_ID) { - source = cereal::SensorEventData::SensorSource::LSM6DS3TRC; - } - - ret = self_test(LSM6DS3_ACCEL_POSITIVE_TEST); - if (ret < 0) { - LOGE("LSM6DS3 accel positive self-test failed!"); - if (do_self_test) goto fail; - } - - ret = self_test(LSM6DS3_ACCEL_NEGATIVE_TEST); - if (ret < 0) { - LOGE("LSM6DS3 accel negative self-test failed!"); - if (do_self_test) goto fail; - } - - ret = init_gpio(); - if (ret < 0) { - goto fail; - } - - // enable continuous update, and automatic increase - ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL3_C, LSM6DS3_ACCEL_IF_INC); - if (ret < 0) { - goto fail; - } - - // TODO: set scale and bandwidth. Default is +- 2G, 50 Hz - ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, LSM6DS3_ACCEL_ODR_104HZ); - if (ret < 0) { - goto fail; - } - - ret = set_register(LSM6DS3_ACCEL_I2C_REG_DRDY_CFG, LSM6DS3_ACCEL_DRDY_PULSE_MODE); - if (ret < 0) { - goto fail; - } - - // enable data ready interrupt for accel on INT1 - // (without resetting existing interrupts) - ret = read_register(LSM6DS3_ACCEL_I2C_REG_INT1_CTRL, &value, 1); - if (ret < 0) { - goto fail; - } - - value |= LSM6DS3_ACCEL_INT1_DRDY_XL; - ret = set_register(LSM6DS3_ACCEL_I2C_REG_INT1_CTRL, value); - -fail: - return ret; -} - -int LSM6DS3_Accel::shutdown() { - int ret = 0; - - // disable data ready interrupt for accel on INT1 - uint8_t value = 0; - ret = read_register(LSM6DS3_ACCEL_I2C_REG_INT1_CTRL, &value, 1); - if (ret < 0) { - goto fail; - } - - value &= ~(LSM6DS3_ACCEL_INT1_DRDY_XL); - ret = set_register(LSM6DS3_ACCEL_I2C_REG_INT1_CTRL, value); - if (ret < 0) { - LOGE("Could not disable lsm6ds3 acceleration interrupt!"); - goto fail; - } - - // enable power-down mode - value = 0; - ret = read_register(LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, &value, 1); - if (ret < 0) { - goto fail; - } - - value &= 0x0F; - ret = set_register(LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, value); - if (ret < 0) { - LOGE("Could not power-down lsm6ds3 accelerometer!"); - goto fail; - } - -fail: - return ret; -} - -bool LSM6DS3_Accel::get_event(MessageBuilder &msg, uint64_t ts) { - - // INT1 shared with gyro, check STATUS_REG who triggered - uint8_t status_reg = 0; - read_register(LSM6DS3_ACCEL_I2C_REG_STAT_REG, &status_reg, sizeof(status_reg)); - if ((status_reg & LSM6DS3_ACCEL_DRDY_XLDA) == 0) { - return false; - } - - uint8_t buffer[6]; - int len = read_register(LSM6DS3_ACCEL_I2C_REG_OUTX_L_XL, buffer, sizeof(buffer)); - assert(len == sizeof(buffer)); - - float scale = 9.81 * 2.0f / (1 << 15); - float x = read_16_bit(buffer[0], buffer[1]) * scale; - float y = read_16_bit(buffer[2], buffer[3]) * scale; - float z = read_16_bit(buffer[4], buffer[5]) * scale; - - auto event = msg.initEvent().initAccelerometer(); - event.setSource(source); - event.setVersion(1); - event.setSensor(SENSOR_ACCELEROMETER); - event.setType(SENSOR_TYPE_ACCELEROMETER); - event.setTimestamp(ts); - - float xyz[] = {y, -x, z}; - auto svec = event.initAcceleration(); - svec.setV(xyz); - svec.setStatus(true); - - return true; -} diff --git a/system/sensord/sensors/lsm6ds3_accel.h b/system/sensord/sensors/lsm6ds3_accel.h deleted file mode 100644 index 69667cb759..0000000000 --- a/system/sensord/sensors/lsm6ds3_accel.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include "system/sensord/sensors/i2c_sensor.h" - -// Address of the chip on the bus -#define LSM6DS3_ACCEL_I2C_ADDR 0x6A - -// Registers of the chip -#define LSM6DS3_ACCEL_I2C_REG_DRDY_CFG 0x0B -#define LSM6DS3_ACCEL_I2C_REG_ID 0x0F -#define LSM6DS3_ACCEL_I2C_REG_INT1_CTRL 0x0D -#define LSM6DS3_ACCEL_I2C_REG_CTRL1_XL 0x10 -#define LSM6DS3_ACCEL_I2C_REG_CTRL3_C 0x12 -#define LSM6DS3_ACCEL_I2C_REG_CTRL5_C 0x14 -#define LSM6DS3_ACCEL_I2C_REG_CTR9_XL 0x18 -#define LSM6DS3_ACCEL_I2C_REG_STAT_REG 0x1E -#define LSM6DS3_ACCEL_I2C_REG_OUTX_L_XL 0x28 - -// Constants -#define LSM6DS3_ACCEL_CHIP_ID 0x69 -#define LSM6DS3TRC_ACCEL_CHIP_ID 0x6A -#define LSM6DS3_ACCEL_FS_4G (0b10 << 2) -#define LSM6DS3_ACCEL_ODR_52HZ (0b0011 << 4) -#define LSM6DS3_ACCEL_ODR_104HZ (0b0100 << 4) -#define LSM6DS3_ACCEL_INT1_DRDY_XL 0b1 -#define LSM6DS3_ACCEL_DRDY_XLDA 0b1 -#define LSM6DS3_ACCEL_DRDY_PULSE_MODE (1 << 7) -#define LSM6DS3_ACCEL_IF_INC 0b00000100 -#define LSM6DS3_ACCEL_IF_INC_BDU 0b01000100 -#define LSM6DS3_ACCEL_XYZ_DEN 0b11100000 -#define LSM6DS3_ACCEL_POSITIVE_TEST 0b01 -#define LSM6DS3_ACCEL_NEGATIVE_TEST 0b10 -#define LSM6DS3_ACCEL_MIN_ST_LIMIT_mg 90.0f -#define LSM6DS3_ACCEL_MAX_ST_LIMIT_mg 1700.0f - -class LSM6DS3_Accel : public I2CSensor { - uint8_t get_device_address() {return LSM6DS3_ACCEL_I2C_ADDR;} - cereal::SensorEventData::SensorSource source = cereal::SensorEventData::SensorSource::LSM6DS3; - - // self test functions - int self_test(int test_type); - void wait_for_data_ready(); - void read_and_avg_data(float* val_st_off); -public: - LSM6DS3_Accel(I2CBus *bus, int gpio_nr = 0, bool shared_gpio = false); - int init(); - bool get_event(MessageBuilder &msg, uint64_t ts = 0); - int shutdown(); -}; diff --git a/system/sensord/sensors/lsm6ds3_accel.py b/system/sensord/sensors/lsm6ds3_accel.py new file mode 100644 index 0000000000..2d788fcbe2 --- /dev/null +++ b/system/sensord/sensors/lsm6ds3_accel.py @@ -0,0 +1,157 @@ +import os +import time + +from cereal import log +from openpilot.system.sensord.sensors.i2c_sensor import Sensor + +class LSM6DS3_Accel(Sensor): + LSM6DS3_ACCEL_I2C_REG_DRDY_CFG = 0x0B + LSM6DS3_ACCEL_I2C_REG_INT1_CTRL = 0x0D + LSM6DS3_ACCEL_I2C_REG_CTRL1_XL = 0x10 + LSM6DS3_ACCEL_I2C_REG_CTRL3_C = 0x12 + LSM6DS3_ACCEL_I2C_REG_CTRL5_C = 0x14 + LSM6DS3_ACCEL_I2C_REG_STAT_REG = 0x1E + LSM6DS3_ACCEL_I2C_REG_OUTX_L_XL = 0x28 + + LSM6DS3_ACCEL_ODR_104HZ = (0b0100 << 4) + LSM6DS3_ACCEL_INT1_DRDY_XL = 0b1 + LSM6DS3_ACCEL_DRDY_XLDA = 0b1 + LSM6DS3_ACCEL_DRDY_PULSE_MODE = (1 << 7) + LSM6DS3_ACCEL_IF_INC = 0b00000100 + + LSM6DS3_ACCEL_ODR_52HZ = (0b0011 << 4) + LSM6DS3_ACCEL_FS_4G = (0b10 << 2) + LSM6DS3_ACCEL_IF_INC_BDU = 0b01000100 + LSM6DS3_ACCEL_POSITIVE_TEST = 0b01 + LSM6DS3_ACCEL_NEGATIVE_TEST = 0b10 + LSM6DS3_ACCEL_MIN_ST_LIMIT_mg = 90.0 + LSM6DS3_ACCEL_MAX_ST_LIMIT_mg = 1700.0 + + @property + def device_address(self) -> int: + return 0x6A + + def init(self): + chip_id = self.verify_chip_id(0x0F, [0x69, 0x6A]) + if chip_id == 0x6A: + self.source = log.SensorEventData.SensorSource.lsm6ds3trc + else: + self.source = log.SensorEventData.SensorSource.lsm6ds3 + + # self-test + if os.getenv("LSM_SELF_TEST") == "1": + self.self_test(self.LSM6DS3_ACCEL_POSITIVE_TEST) + self.self_test(self.LSM6DS3_ACCEL_NEGATIVE_TEST) + + # actual init + int1 = self.read(self.LSM6DS3_ACCEL_I2C_REG_INT1_CTRL, 1)[0] + int1 |= self.LSM6DS3_ACCEL_INT1_DRDY_XL + self.writes(( + # Enable continuous update and automatic address increment + (self.LSM6DS3_ACCEL_I2C_REG_CTRL3_C, self.LSM6DS3_ACCEL_IF_INC), + # Set ODR to 104 Hz, FS to ±2g (default) + (self.LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, self.LSM6DS3_ACCEL_ODR_104HZ), + # Configure data ready signal to pulse mode + (self.LSM6DS3_ACCEL_I2C_REG_DRDY_CFG, self.LSM6DS3_ACCEL_DRDY_PULSE_MODE), + # Enable data ready interrupt on INT1 without resetting existing interrupts + (self.LSM6DS3_ACCEL_I2C_REG_INT1_CTRL, int1), + )) + + def get_event(self, ts: int | None = None) -> log.SensorEventData: + assert ts is not None # must come from the IRQ event + + # Check if data is ready since IRQ is shared with gyro + status_reg = self.read(self.LSM6DS3_ACCEL_I2C_REG_STAT_REG, 1)[0] + if (status_reg & self.LSM6DS3_ACCEL_DRDY_XLDA) == 0: + raise self.DataNotReady + + scale = 9.81 * 2.0 / (1 << 15) + b = self.read(self.LSM6DS3_ACCEL_I2C_REG_OUTX_L_XL, 6) + x = self.parse_16bit(b[0], b[1]) * scale + y = self.parse_16bit(b[2], b[3]) * scale + z = self.parse_16bit(b[4], b[5]) * scale + + event = log.SensorEventData.new_message() + event.timestamp = ts + event.version = 1 + event.sensor = 1 # SENSOR_ACCELEROMETER + event.type = 1 # SENSOR_TYPE_ACCELEROMETER + event.source = self.source + a = event.init('acceleration') + a.v = [y, -x, z] + a.status = 1 + return event + + def shutdown(self) -> None: + # Disable data ready interrupt on INT1 + value = self.read(self.LSM6DS3_ACCEL_I2C_REG_INT1_CTRL, 1)[0] + value &= ~self.LSM6DS3_ACCEL_INT1_DRDY_XL + self.write(self.LSM6DS3_ACCEL_I2C_REG_INT1_CTRL, value) + + # Power down by clearing ODR bits + value = self.read(self.LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, 1)[0] + value &= 0x0F + self.write(self.LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, value) + + # *** self-test stuff *** + def _wait_for_data_ready(self): + while True: + drdy = self.read(self.LSM6DS3_ACCEL_I2C_REG_STAT_REG, 1)[0] + if drdy & self.LSM6DS3_ACCEL_DRDY_XLDA: + break + + def _read_and_avg_data(self, scaling: float) -> list[float]: + out_buf = [0.0, 0.0, 0.0] + for _ in range(5): + self._wait_for_data_ready() + b = self.read(self.LSM6DS3_ACCEL_I2C_REG_OUTX_L_XL, 6) + for j in range(3): + val = self.parse_16bit(b[j*2], b[j*2+1]) * scaling + out_buf[j] += val + return [x / 5.0 for x in out_buf] + + def self_test(self, test_type: int) -> None: + # Prepare sensor for self-test + self.write(self.LSM6DS3_ACCEL_I2C_REG_CTRL3_C, self.LSM6DS3_ACCEL_IF_INC_BDU) + + # Configure ODR and full scale based on sensor type + if self.source == log.SensorEventData.SensorSource.lsm6ds3trc: + odr_fs = self.LSM6DS3_ACCEL_FS_4G | self.LSM6DS3_ACCEL_ODR_52HZ + scaling = 0.122 # mg/LSB for ±4g + else: + odr_fs = self.LSM6DS3_ACCEL_ODR_52HZ + scaling = 0.061 # mg/LSB for ±2g + self.write(self.LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, odr_fs) + + # Wait for stable output + time.sleep(0.1) + self._wait_for_data_ready() + val_st_off = self._read_and_avg_data(scaling) + + # Enable self-test + self.write(self.LSM6DS3_ACCEL_I2C_REG_CTRL5_C, test_type) + + # Wait for stable output + time.sleep(0.1) + self._wait_for_data_ready() + val_st_on = self._read_and_avg_data(scaling) + + # Disable sensor and self-test + self.write(self.LSM6DS3_ACCEL_I2C_REG_CTRL1_XL, 0) + self.write(self.LSM6DS3_ACCEL_I2C_REG_CTRL5_C, 0) + + # Calculate differences and check limits + test_val = [abs(on - off) for on, off in zip(val_st_on, val_st_off, strict=False)] + for val in test_val: + if val < self.LSM6DS3_ACCEL_MIN_ST_LIMIT_mg or val > self.LSM6DS3_ACCEL_MAX_ST_LIMIT_mg: + raise self.SensorException(f"Accelerometer self-test failed for test type {test_type}") + +if __name__ == "__main__": + import numpy as np + s = LSM6DS3_Accel(1) + s.init() + time.sleep(0.2) + e = s.get_event(0) + print(e) + print(np.linalg.norm(e.acceleration.v)) + s.shutdown() diff --git a/system/sensord/sensors/lsm6ds3_gyro.cc b/system/sensord/sensors/lsm6ds3_gyro.cc deleted file mode 100644 index bb560edeab..0000000000 --- a/system/sensord/sensors/lsm6ds3_gyro.cc +++ /dev/null @@ -1,233 +0,0 @@ -#include "system/sensord/sensors/lsm6ds3_gyro.h" - -#include -#include -#include - -#include "common/swaglog.h" -#include "common/timing.h" -#include "common/util.h" - -#define DEG2RAD(x) ((x) * M_PI / 180.0) - -LSM6DS3_Gyro::LSM6DS3_Gyro(I2CBus *bus, int gpio_nr, bool shared_gpio) : - I2CSensor(bus, gpio_nr, shared_gpio) {} - -void LSM6DS3_Gyro::wait_for_data_ready() { - uint8_t drdy = 0; - uint8_t buffer[6]; - - do { - read_register(LSM6DS3_GYRO_I2C_REG_STAT_REG, &drdy, sizeof(drdy)); - drdy &= LSM6DS3_GYRO_DRDY_GDA; - } while (drdy == 0); - - read_register(LSM6DS3_GYRO_I2C_REG_OUTX_L_G, buffer, sizeof(buffer)); -} - -void LSM6DS3_Gyro::read_and_avg_data(float* out_buf) { - uint8_t drdy = 0; - uint8_t buffer[6]; - - for (int i = 0; i < 5; i++) { - do { - read_register(LSM6DS3_GYRO_I2C_REG_STAT_REG, &drdy, sizeof(drdy)); - drdy &= LSM6DS3_GYRO_DRDY_GDA; - } while (drdy == 0); - - int len = read_register(LSM6DS3_GYRO_I2C_REG_OUTX_L_G, buffer, sizeof(buffer)); - assert(len == sizeof(buffer)); - - for (int j = 0; j < 3; j++) { - out_buf[j] += (float)read_16_bit(buffer[j*2], buffer[j*2+1]) * 70.0f; - } - } - - // calculate the mg average values - for (int i = 0; i < 3; i++) { - out_buf[i] /= 5.0f; - } -} - -int LSM6DS3_Gyro::self_test(int test_type) { - float val_st_off[3] = {0}; - float val_st_on[3] = {0}; - float test_val[3] = {0}; - - // prepare sensor for self-test - - // full scale: 2000dps, ODR: 208Hz - int ret = set_register(LSM6DS3_GYRO_I2C_REG_CTRL2_G, LSM6DS3_GYRO_ODR_208HZ | LSM6DS3_GYRO_FS_2000dps); - if (ret < 0) { - return ret; - } - - // wait for stable output, and discard first values - util::sleep_for(150); - wait_for_data_ready(); - read_and_avg_data(val_st_off); - - // enable Self Test positive (or negative) - ret = set_register(LSM6DS3_GYRO_I2C_REG_CTRL5_C, test_type); - if (ret < 0) { - return ret; - } - - // wait for stable output, and discard first values - util::sleep_for(50); - wait_for_data_ready(); - read_and_avg_data(val_st_on); - - // disable sensor - ret = set_register(LSM6DS3_GYRO_I2C_REG_CTRL2_G, 0); - if (ret < 0) { - return ret; - } - - // disable self test - ret = set_register(LSM6DS3_GYRO_I2C_REG_CTRL5_C, 0); - if (ret < 0) { - return ret; - } - - // calculate the mg values for self test - for (int i = 0; i < 3; i++) { - test_val[i] = fabs(val_st_on[i] - val_st_off[i]); - } - - // verify test result - for (int i = 0; i < 3; i++) { - if ((LSM6DS3_GYRO_MIN_ST_LIMIT_mdps > test_val[i]) || - (test_val[i] > LSM6DS3_GYRO_MAX_ST_LIMIT_mdps)) { - return -1; - } - } - - return ret; -} - -int LSM6DS3_Gyro::init() { - uint8_t value = 0; - bool do_self_test = false; - - const char* env_lsm_selftest = std::getenv("LSM_SELF_TEST"); - if (env_lsm_selftest != nullptr && strncmp(env_lsm_selftest, "1", 1) == 0) { - do_self_test = true; - } - - int ret = verify_chip_id(LSM6DS3_GYRO_I2C_REG_ID, {LSM6DS3_GYRO_CHIP_ID, LSM6DS3TRC_GYRO_CHIP_ID}); - if (ret == -1) return -1; - - if (ret == LSM6DS3TRC_GYRO_CHIP_ID) { - source = cereal::SensorEventData::SensorSource::LSM6DS3TRC; - } - - ret = init_gpio(); - if (ret < 0) { - goto fail; - } - - ret = self_test(LSM6DS3_GYRO_POSITIVE_TEST); - if (ret < 0) { - LOGE("LSM6DS3 gyro positive self-test failed!"); - if (do_self_test) goto fail; - } - - ret = self_test(LSM6DS3_GYRO_NEGATIVE_TEST); - if (ret < 0) { - LOGE("LSM6DS3 gyro negative self-test failed!"); - if (do_self_test) goto fail; - } - - // TODO: set scale. Default is +- 250 deg/s - ret = set_register(LSM6DS3_GYRO_I2C_REG_CTRL2_G, LSM6DS3_GYRO_ODR_104HZ); - if (ret < 0) { - goto fail; - } - - ret = set_register(LSM6DS3_GYRO_I2C_REG_DRDY_CFG, LSM6DS3_GYRO_DRDY_PULSE_MODE); - if (ret < 0) { - goto fail; - } - - // enable data ready interrupt for gyro on INT1 - // (without resetting existing interrupts) - ret = read_register(LSM6DS3_GYRO_I2C_REG_INT1_CTRL, &value, 1); - if (ret < 0) { - goto fail; - } - - value |= LSM6DS3_GYRO_INT1_DRDY_G; - ret = set_register(LSM6DS3_GYRO_I2C_REG_INT1_CTRL, value); - -fail: - return ret; -} - -int LSM6DS3_Gyro::shutdown() { - int ret = 0; - - // disable data ready interrupt for gyro on INT1 - uint8_t value = 0; - ret = read_register(LSM6DS3_GYRO_I2C_REG_INT1_CTRL, &value, 1); - if (ret < 0) { - goto fail; - } - - value &= ~(LSM6DS3_GYRO_INT1_DRDY_G); - ret = set_register(LSM6DS3_GYRO_I2C_REG_INT1_CTRL, value); - if (ret < 0) { - LOGE("Could not disable lsm6ds3 gyroscope interrupt!"); - goto fail; - } - - // enable power-down mode - value = 0; - ret = read_register(LSM6DS3_GYRO_I2C_REG_CTRL2_G, &value, 1); - if (ret < 0) { - goto fail; - } - - value &= 0x0F; - ret = set_register(LSM6DS3_GYRO_I2C_REG_CTRL2_G, value); - if (ret < 0) { - LOGE("Could not power-down lsm6ds3 gyroscope!"); - goto fail; - } - -fail: - return ret; -} - -bool LSM6DS3_Gyro::get_event(MessageBuilder &msg, uint64_t ts) { - - // INT1 shared with accel, check STATUS_REG who triggered - uint8_t status_reg = 0; - read_register(LSM6DS3_GYRO_I2C_REG_STAT_REG, &status_reg, sizeof(status_reg)); - if ((status_reg & LSM6DS3_GYRO_DRDY_GDA) == 0) { - return false; - } - - uint8_t buffer[6]; - int len = read_register(LSM6DS3_GYRO_I2C_REG_OUTX_L_G, buffer, sizeof(buffer)); - assert(len == sizeof(buffer)); - - float scale = 8.75 / 1000.0; - float x = DEG2RAD(read_16_bit(buffer[0], buffer[1]) * scale); - float y = DEG2RAD(read_16_bit(buffer[2], buffer[3]) * scale); - float z = DEG2RAD(read_16_bit(buffer[4], buffer[5]) * scale); - - auto event = msg.initEvent().initGyroscope(); - event.setSource(source); - event.setVersion(2); - event.setSensor(SENSOR_GYRO_UNCALIBRATED); - event.setType(SENSOR_TYPE_GYROSCOPE_UNCALIBRATED); - event.setTimestamp(ts); - - float xyz[] = {y, -x, z}; - auto svec = event.initGyroUncalibrated(); - svec.setV(xyz); - svec.setStatus(true); - - return true; -} diff --git a/system/sensord/sensors/lsm6ds3_gyro.h b/system/sensord/sensors/lsm6ds3_gyro.h deleted file mode 100644 index adaae62dd2..0000000000 --- a/system/sensord/sensors/lsm6ds3_gyro.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include "system/sensord/sensors/i2c_sensor.h" - -// Address of the chip on the bus -#define LSM6DS3_GYRO_I2C_ADDR 0x6A - -// Registers of the chip -#define LSM6DS3_GYRO_I2C_REG_DRDY_CFG 0x0B -#define LSM6DS3_GYRO_I2C_REG_ID 0x0F -#define LSM6DS3_GYRO_I2C_REG_INT1_CTRL 0x0D -#define LSM6DS3_GYRO_I2C_REG_CTRL2_G 0x11 -#define LSM6DS3_GYRO_I2C_REG_CTRL5_C 0x14 -#define LSM6DS3_GYRO_I2C_REG_STAT_REG 0x1E -#define LSM6DS3_GYRO_I2C_REG_OUTX_L_G 0x22 -#define LSM6DS3_GYRO_POSITIVE_TEST (0b01 << 2) -#define LSM6DS3_GYRO_NEGATIVE_TEST (0b11 << 2) - -// Constants -#define LSM6DS3_GYRO_CHIP_ID 0x69 -#define LSM6DS3TRC_GYRO_CHIP_ID 0x6A -#define LSM6DS3_GYRO_FS_2000dps (0b11 << 2) -#define LSM6DS3_GYRO_ODR_104HZ (0b0100 << 4) -#define LSM6DS3_GYRO_ODR_208HZ (0b0101 << 4) -#define LSM6DS3_GYRO_INT1_DRDY_G 0b10 -#define LSM6DS3_GYRO_DRDY_GDA 0b10 -#define LSM6DS3_GYRO_DRDY_PULSE_MODE (1 << 7) -#define LSM6DS3_GYRO_MIN_ST_LIMIT_mdps 150000.0f -#define LSM6DS3_GYRO_MAX_ST_LIMIT_mdps 700000.0f - - -class LSM6DS3_Gyro : public I2CSensor { - uint8_t get_device_address() {return LSM6DS3_GYRO_I2C_ADDR;} - cereal::SensorEventData::SensorSource source = cereal::SensorEventData::SensorSource::LSM6DS3; - - // self test functions - int self_test(int test_type); - void wait_for_data_ready(); - void read_and_avg_data(float* val_st_off); -public: - LSM6DS3_Gyro(I2CBus *bus, int gpio_nr = 0, bool shared_gpio = false); - int init(); - bool get_event(MessageBuilder &msg, uint64_t ts = 0); - int shutdown(); -}; diff --git a/system/sensord/sensors/lsm6ds3_gyro.py b/system/sensord/sensors/lsm6ds3_gyro.py new file mode 100644 index 0000000000..68fd267df2 --- /dev/null +++ b/system/sensord/sensors/lsm6ds3_gyro.py @@ -0,0 +1,141 @@ +import os +import math +import time + +from cereal import log +from openpilot.system.sensord.sensors.i2c_sensor import Sensor + +class LSM6DS3_Gyro(Sensor): + LSM6DS3_GYRO_I2C_REG_DRDY_CFG = 0x0B + LSM6DS3_GYRO_I2C_REG_INT1_CTRL = 0x0D + LSM6DS3_GYRO_I2C_REG_CTRL2_G = 0x11 + LSM6DS3_GYRO_I2C_REG_CTRL5_C = 0x14 + LSM6DS3_GYRO_I2C_REG_STAT_REG = 0x1E + LSM6DS3_GYRO_I2C_REG_OUTX_L_G = 0x22 + + LSM6DS3_GYRO_ODR_104HZ = (0b0100 << 4) + LSM6DS3_GYRO_INT1_DRDY_G = 0b10 + LSM6DS3_GYRO_DRDY_GDA = 0b10 + LSM6DS3_GYRO_DRDY_PULSE_MODE = (1 << 7) + + LSM6DS3_GYRO_ODR_208HZ = (0b0101 << 4) + LSM6DS3_GYRO_FS_2000dps = (0b11 << 2) + LSM6DS3_GYRO_POSITIVE_TEST = (0b01 << 2) + LSM6DS3_GYRO_NEGATIVE_TEST = (0b11 << 2) + LSM6DS3_GYRO_MIN_ST_LIMIT_mdps = 150000.0 + LSM6DS3_GYRO_MAX_ST_LIMIT_mdps = 700000.0 + + @property + def device_address(self) -> int: + return 0x6A + + def init(self): + chip_id = self.verify_chip_id(0x0F, [0x69, 0x6A]) + if chip_id == 0x6A: + self.source = log.SensorEventData.SensorSource.lsm6ds3trc + else: + self.source = log.SensorEventData.SensorSource.lsm6ds3 + + # self-test + if "LSM_SELF_TEST" in os.environ: + self.self_test(self.LSM6DS3_GYRO_POSITIVE_TEST) + self.self_test(self.LSM6DS3_GYRO_NEGATIVE_TEST) + + # actual init + self.writes(( + # TODO: set scale. Default is +- 250 deg/s + (self.LSM6DS3_GYRO_I2C_REG_CTRL2_G, self.LSM6DS3_GYRO_ODR_104HZ), + # Configure data ready signal to pulse mode + (self.LSM6DS3_GYRO_I2C_REG_DRDY_CFG, self.LSM6DS3_GYRO_DRDY_PULSE_MODE), + )) + value = self.read(self.LSM6DS3_GYRO_I2C_REG_INT1_CTRL, 1)[0] + value |= self.LSM6DS3_GYRO_INT1_DRDY_G + self.write(self.LSM6DS3_GYRO_I2C_REG_INT1_CTRL, value) + + def get_event(self, ts: int | None = None) -> log.SensorEventData: + assert ts is not None # must come from the IRQ event + + # Check if gyroscope data is ready, since it's shared with accelerometer + status_reg = self.read(self.LSM6DS3_GYRO_I2C_REG_STAT_REG, 1)[0] + if not (status_reg & self.LSM6DS3_GYRO_DRDY_GDA): + raise self.DataNotReady + + b = self.read(self.LSM6DS3_GYRO_I2C_REG_OUTX_L_G, 6) + x = self.parse_16bit(b[0], b[1]) + y = self.parse_16bit(b[2], b[3]) + z = self.parse_16bit(b[4], b[5]) + scale = (8.75 / 1000.0) * (math.pi / 180.0) + xyz = [y * scale, -x * scale, z * scale] + + event = log.SensorEventData.new_message() + event.timestamp = ts + event.version = 2 + event.sensor = 5 # SENSOR_GYRO_UNCALIBRATED + event.type = 16 # SENSOR_TYPE_GYROSCOPE_UNCALIBRATED + event.source = self.source + g = event.init('gyroUncalibrated') + g.v = xyz + g.status = 1 + return event + + def shutdown(self) -> None: + # Disable data ready interrupt on INT1 + value = self.read(self.LSM6DS3_GYRO_I2C_REG_INT1_CTRL, 1)[0] + value &= ~self.LSM6DS3_GYRO_INT1_DRDY_G + self.write(self.LSM6DS3_GYRO_I2C_REG_INT1_CTRL, value) + + # Power down by clearing ODR bits + value = self.read(self.LSM6DS3_GYRO_I2C_REG_CTRL2_G, 1)[0] + value &= 0x0F + self.write(self.LSM6DS3_GYRO_I2C_REG_CTRL2_G, value) + + # *** self-test stuff *** + def _wait_for_data_ready(self): + while True: + drdy = self.read(self.LSM6DS3_GYRO_I2C_REG_STAT_REG, 1)[0] + if drdy & self.LSM6DS3_GYRO_DRDY_GDA: + break + + def _read_and_avg_data(self) -> list[float]: + out_buf = [0.0, 0.0, 0.0] + for _ in range(5): + self._wait_for_data_ready() + b = self.read(self.LSM6DS3_GYRO_I2C_REG_OUTX_L_G, 6) + for j in range(3): + val = self.parse_16bit(b[j*2], b[j*2+1]) * 70.0 # mdps/LSB for 2000 dps + out_buf[j] += val + return [x / 5.0 for x in out_buf] + + def self_test(self, test_type: int): + # Set ODR to 208Hz, FS to 2000dps + self.write(self.LSM6DS3_GYRO_I2C_REG_CTRL2_G, self.LSM6DS3_GYRO_ODR_208HZ | self.LSM6DS3_GYRO_FS_2000dps) + + # Wait for stable output + time.sleep(0.15) + self._wait_for_data_ready() + val_st_off = self._read_and_avg_data() + + # Enable self-test + self.write(self.LSM6DS3_GYRO_I2C_REG_CTRL5_C, test_type) + + # Wait for stable output + time.sleep(0.05) + self._wait_for_data_ready() + val_st_on = self._read_and_avg_data() + + # Disable sensor and self-test + self.write(self.LSM6DS3_GYRO_I2C_REG_CTRL2_G, 0) + self.write(self.LSM6DS3_GYRO_I2C_REG_CTRL5_C, 0) + + # Calculate differences and check limits + test_val = [abs(on - off) for on, off in zip(val_st_on, val_st_off, strict=False)] + for val in test_val: + if val < self.LSM6DS3_GYRO_MIN_ST_LIMIT_mdps or val > self.LSM6DS3_GYRO_MAX_ST_LIMIT_mdps: + raise Exception(f"Gyroscope self-test failed for test type {test_type}") + +if __name__ == "__main__": + s = LSM6DS3_Gyro(1) + s.init() + time.sleep(0.1) + print(s.get_event(0)) + s.shutdown() diff --git a/system/sensord/sensors/lsm6ds3_temp.cc b/system/sensord/sensors/lsm6ds3_temp.cc deleted file mode 100644 index f481614154..0000000000 --- a/system/sensord/sensors/lsm6ds3_temp.cc +++ /dev/null @@ -1,37 +0,0 @@ -#include "system/sensord/sensors/lsm6ds3_temp.h" - -#include - -#include "common/swaglog.h" -#include "common/timing.h" - -LSM6DS3_Temp::LSM6DS3_Temp(I2CBus *bus) : I2CSensor(bus) {} - -int LSM6DS3_Temp::init() { - int ret = verify_chip_id(LSM6DS3_TEMP_I2C_REG_ID, {LSM6DS3_TEMP_CHIP_ID, LSM6DS3TRC_TEMP_CHIP_ID}); - if (ret == -1) return -1; - - if (ret == LSM6DS3TRC_TEMP_CHIP_ID) { - source = cereal::SensorEventData::SensorSource::LSM6DS3TRC; - } - return 0; -} - -bool LSM6DS3_Temp::get_event(MessageBuilder &msg, uint64_t ts) { - uint64_t start_time = nanos_since_boot(); - uint8_t buffer[2]; - int len = read_register(LSM6DS3_TEMP_I2C_REG_OUT_TEMP_L, buffer, sizeof(buffer)); - assert(len == sizeof(buffer)); - - float scale = (source == cereal::SensorEventData::SensorSource::LSM6DS3TRC) ? 256.0f : 16.0f; - float temp = 25.0f + read_16_bit(buffer[0], buffer[1]) / scale; - - auto event = msg.initEvent().initTemperatureSensor(); - event.setSource(source); - event.setVersion(1); - event.setType(SENSOR_TYPE_AMBIENT_TEMPERATURE); - event.setTimestamp(start_time); - event.setTemperature(temp); - - return true; -} diff --git a/system/sensord/sensors/lsm6ds3_temp.h b/system/sensord/sensors/lsm6ds3_temp.h deleted file mode 100644 index 1b5b621814..0000000000 --- a/system/sensord/sensors/lsm6ds3_temp.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "system/sensord/sensors/i2c_sensor.h" - -// Address of the chip on the bus -#define LSM6DS3_TEMP_I2C_ADDR 0x6A - -// Registers of the chip -#define LSM6DS3_TEMP_I2C_REG_ID 0x0F -#define LSM6DS3_TEMP_I2C_REG_OUT_TEMP_L 0x20 - -// Constants -#define LSM6DS3_TEMP_CHIP_ID 0x69 -#define LSM6DS3TRC_TEMP_CHIP_ID 0x6A - - -class LSM6DS3_Temp : public I2CSensor { - uint8_t get_device_address() {return LSM6DS3_TEMP_I2C_ADDR;} - cereal::SensorEventData::SensorSource source = cereal::SensorEventData::SensorSource::LSM6DS3; - -public: - LSM6DS3_Temp(I2CBus *bus); - int init(); - bool get_event(MessageBuilder &msg, uint64_t ts = 0); - int shutdown() { return 0; } -}; diff --git a/system/sensord/sensors/lsm6ds3_temp.py b/system/sensord/sensors/lsm6ds3_temp.py new file mode 100644 index 0000000000..7d7b1c2ce1 --- /dev/null +++ b/system/sensord/sensors/lsm6ds3_temp.py @@ -0,0 +1,33 @@ +import time + +from cereal import log +from openpilot.system.sensord.sensors.i2c_sensor import Sensor + +# https://content.arduino.cc/assets/st_imu_lsm6ds3_datasheet.pdf +class LSM6DS3_Temp(Sensor): + @property + def device_address(self) -> int: + return 0x6A # Default I2C address for LSM6DS3 + + def _read_temperature(self) -> float: + scale = 16.0 if log.SensorEventData.SensorSource.lsm6ds3 else 256.0 + data = self.read(0x20, 2) + return 25 + (self.parse_16bit(data[0], data[1]) / scale) + + def init(self): + chip_id = self.verify_chip_id(0x0F, [0x69, 0x6A]) + if chip_id == 0x6A: + self.source = log.SensorEventData.SensorSource.lsm6ds3trc + else: + self.source = log.SensorEventData.SensorSource.lsm6ds3 + + def get_event(self, ts: int | None = None) -> log.SensorEventData: + event = log.SensorEventData.new_message() + event.version = 1 + event.timestamp = int(time.monotonic() * 1e9) + event.source = self.source + event.temperature = self._read_temperature() + return event + + def shutdown(self) -> None: + pass diff --git a/system/sensord/sensors/mmc5603nj_magn.cc b/system/sensord/sensors/mmc5603nj_magn.cc deleted file mode 100644 index 0e8ba967e3..0000000000 --- a/system/sensord/sensors/mmc5603nj_magn.cc +++ /dev/null @@ -1,108 +0,0 @@ -#include "system/sensord/sensors/mmc5603nj_magn.h" - -#include -#include -#include - -#include "common/swaglog.h" -#include "common/timing.h" -#include "common/util.h" - -MMC5603NJ_Magn::MMC5603NJ_Magn(I2CBus *bus) : I2CSensor(bus) {} - -int MMC5603NJ_Magn::init() { - int ret = verify_chip_id(MMC5603NJ_I2C_REG_ID, {MMC5603NJ_CHIP_ID}); - if (ret == -1) return -1; - - // Set ODR to 0 - ret = set_register(MMC5603NJ_I2C_REG_ODR, 0); - if (ret < 0) { - goto fail; - } - - // Set BW to 0b01 for 1-150 Hz operation - ret = set_register(MMC5603NJ_I2C_REG_INTERNAL_1, 0b01); - if (ret < 0) { - goto fail; - } - -fail: - return ret; -} - -int MMC5603NJ_Magn::shutdown() { - int ret = 0; - - // disable auto reset of measurements - uint8_t value = 0; - ret = read_register(MMC5603NJ_I2C_REG_INTERNAL_0, &value, 1); - if (ret < 0) { - goto fail; - } - - value &= ~(MMC5603NJ_CMM_FREQ_EN | MMC5603NJ_AUTO_SR_EN); - ret = set_register(MMC5603NJ_I2C_REG_INTERNAL_0, value); - if (ret < 0) { - goto fail; - } - - // set ODR to 0 to leave continuous mode - ret = set_register(MMC5603NJ_I2C_REG_ODR, 0); - if (ret < 0) { - goto fail; - } - return ret; - -fail: - LOGE("Could not disable mmc5603nj auto set reset"); - return ret; -} - -void MMC5603NJ_Magn::start_measurement() { - set_register(MMC5603NJ_I2C_REG_INTERNAL_0, 0b01); - util::sleep_for(5); -} - -std::vector MMC5603NJ_Magn::read_measurement() { - int len; - uint8_t buffer[9]; - len = read_register(MMC5603NJ_I2C_REG_XOUT0, buffer, sizeof(buffer)); - assert(len == sizeof(buffer)); - float scale = 1.0 / 16384.0; - float x = (read_20_bit(buffer[6], buffer[1], buffer[0]) * scale) - 32.0; - float y = (read_20_bit(buffer[7], buffer[3], buffer[2]) * scale) - 32.0; - float z = (read_20_bit(buffer[8], buffer[5], buffer[4]) * scale) - 32.0; - std::vector xyz = {x, y, z}; - return xyz; -} - -bool MMC5603NJ_Magn::get_event(MessageBuilder &msg, uint64_t ts) { - uint64_t start_time = nanos_since_boot(); - // SET - RESET cycle - set_register(MMC5603NJ_I2C_REG_INTERNAL_0, MMC5603NJ_SET); - util::sleep_for(5); - MMC5603NJ_Magn::start_measurement(); - std::vector xyz = MMC5603NJ_Magn::read_measurement(); - - set_register(MMC5603NJ_I2C_REG_INTERNAL_0, MMC5603NJ_RESET); - util::sleep_for(5); - MMC5603NJ_Magn::start_measurement(); - std::vector reset_xyz = MMC5603NJ_Magn::read_measurement(); - - auto event = msg.initEvent().initMagnetometer(); - event.setSource(cereal::SensorEventData::SensorSource::MMC5603NJ); - event.setVersion(1); - event.setSensor(SENSOR_MAGNETOMETER_UNCALIBRATED); - event.setType(SENSOR_TYPE_MAGNETIC_FIELD_UNCALIBRATED); - event.setTimestamp(start_time); - - float vals[] = {xyz[0], xyz[1], xyz[2], reset_xyz[0], reset_xyz[1], reset_xyz[2]}; - bool valid = true; - if (std::any_of(std::begin(vals), std::end(vals), [](float val) { return val == -32.0; })) { - valid = false; - } - auto svec = event.initMagneticUncalibrated(); - svec.setV(vals); - svec.setStatus(valid); - return true; -} diff --git a/system/sensord/sensors/mmc5603nj_magn.h b/system/sensord/sensors/mmc5603nj_magn.h deleted file mode 100644 index 9c0fbd2521..0000000000 --- a/system/sensord/sensors/mmc5603nj_magn.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include - -#include "system/sensord/sensors/i2c_sensor.h" - -// Address of the chip on the bus -#define MMC5603NJ_I2C_ADDR 0x30 - -// Registers of the chip -#define MMC5603NJ_I2C_REG_XOUT0 0x00 -#define MMC5603NJ_I2C_REG_ODR 0x1A -#define MMC5603NJ_I2C_REG_INTERNAL_0 0x1B -#define MMC5603NJ_I2C_REG_INTERNAL_1 0x1C -#define MMC5603NJ_I2C_REG_INTERNAL_2 0x1D -#define MMC5603NJ_I2C_REG_ID 0x39 - -// Constants -#define MMC5603NJ_CHIP_ID 0x10 -#define MMC5603NJ_CMM_FREQ_EN (1 << 7) -#define MMC5603NJ_AUTO_SR_EN (1 << 5) -#define MMC5603NJ_CMM_EN (1 << 4) -#define MMC5603NJ_EN_PRD_SET (1 << 3) -#define MMC5603NJ_SET (1 << 3) -#define MMC5603NJ_RESET (1 << 4) - -class MMC5603NJ_Magn : public I2CSensor { -private: - uint8_t get_device_address() {return MMC5603NJ_I2C_ADDR;} - void start_measurement(); - std::vector read_measurement(); -public: - MMC5603NJ_Magn(I2CBus *bus); - int init(); - bool get_event(MessageBuilder &msg, uint64_t ts = 0); - int shutdown(); -}; diff --git a/system/sensord/sensors/mmc5603nj_magn.py b/system/sensord/sensors/mmc5603nj_magn.py new file mode 100644 index 0000000000..255e99eb3e --- /dev/null +++ b/system/sensord/sensors/mmc5603nj_magn.py @@ -0,0 +1,76 @@ +import time + +from cereal import log +from openpilot.system.sensord.sensors.i2c_sensor import Sensor + +# https://www.mouser.com/datasheet/2/821/Memsic_09102019_Datasheet_Rev.B-1635324.pdf + +# Register addresses +REG_ODR = 0x1A +REG_INTERNAL_0 = 0x1B +REG_INTERNAL_1 = 0x1C + +# Control register settings +CMM_FREQ_EN = (1 << 7) +AUTO_SR_EN = (1 << 5) +SET = (1 << 3) +RESET = (1 << 4) + +class MMC5603NJ_Magn(Sensor): + @property + def device_address(self) -> int: + return 0x30 + + def init(self): + self.verify_chip_id(0x39, [0x10, ]) + self.writes(( + (REG_ODR, 0), + + # Set BW to 0b01 for 1-150 Hz operation + (REG_INTERNAL_1, 0b01), + )) + + def _read_data(self, cycle) -> list[float]: + # start measurement + self.write(REG_INTERNAL_0, cycle) + self.wait() + + # read out XYZ + scale = 1.0 / 16384.0 + b = self.read(0x00, 9) + return [ + (self.parse_20bit(b[6], b[1], b[0]) * scale) - 32.0, + (self.parse_20bit(b[7], b[3], b[2]) * scale) - 32.0, + (self.parse_20bit(b[8], b[5], b[4]) * scale) - 32.0, + ] + + def get_event(self, ts: int | None = None) -> log.SensorEventData: + ts = time.monotonic_ns() + + # SET - RESET cycle + xyz = self._read_data(SET) + reset_xyz = self._read_data(RESET) + vals = [*xyz, *reset_xyz] + + event = log.SensorEventData.new_message() + event.timestamp = ts + event.version = 1 + event.sensor = 3 # SENSOR_MAGNETOMETER_UNCALIBRATED + event.type = 14 # SENSOR_TYPE_MAGNETIC_FIELD_UNCALIBRATED + event.source = log.SensorEventData.SensorSource.mmc5603nj + + m = event.init('magneticUncalibrated') + m.v = vals + m.status = int(all(int(v) != -32 for v in vals)) + + return event + + def shutdown(self) -> None: + v = self.read(REG_INTERNAL_0, 1)[0] + self.writes(( + # disable auto-reset of measurements + (REG_INTERNAL_0, (v & (~(CMM_FREQ_EN | AUTO_SR_EN)))), + + # disable continuous mode + (REG_ODR, 0), + )) diff --git a/system/sensord/sensors/sensor.h b/system/sensord/sensors/sensor.h deleted file mode 100644 index ccf998d161..0000000000 --- a/system/sensord/sensors/sensor.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "cereal/messaging/messaging.h" - -class Sensor { -public: - int gpio_fd = -1; - bool enabled = false; - uint64_t start_ts = 0; - uint64_t init_delay = 500e6; // default dealy 500ms - - virtual ~Sensor() {} - virtual int init() = 0; - virtual bool get_event(MessageBuilder &msg, uint64_t ts = 0) = 0; - virtual bool has_interrupt_enabled() = 0; - virtual int shutdown() = 0; - - virtual bool is_data_valid(uint64_t current_ts) { - if (start_ts == 0) { - start_ts = current_ts; - } - return (current_ts - start_ts) > init_delay; - } -}; diff --git a/system/sensord/sensors_qcom2.cc b/system/sensord/sensors_qcom2.cc deleted file mode 100644 index f9f51539c9..0000000000 --- a/system/sensord/sensors_qcom2.cc +++ /dev/null @@ -1,179 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include - -#include "cereal/services.h" -#include "cereal/messaging/messaging.h" -#include "common/i2c.h" -#include "common/ratekeeper.h" -#include "common/swaglog.h" -#include "common/timing.h" -#include "common/util.h" -#include "system/sensord/sensors/bmx055_accel.h" -#include "system/sensord/sensors/bmx055_gyro.h" -#include "system/sensord/sensors/bmx055_magn.h" -#include "system/sensord/sensors/bmx055_temp.h" -#include "system/sensord/sensors/constants.h" -#include "system/sensord/sensors/lsm6ds3_accel.h" -#include "system/sensord/sensors/lsm6ds3_gyro.h" -#include "system/sensord/sensors/lsm6ds3_temp.h" -#include "system/sensord/sensors/mmc5603nj_magn.h" - -#define I2C_BUS_IMU 1 - -ExitHandler do_exit; - -void interrupt_loop(std::vector> sensors) { - PubMaster pm({"gyroscope", "accelerometer"}); - - int fd = -1; - for (auto &[sensor, msg_name] : sensors) { - if (sensor->has_interrupt_enabled()) { - fd = sensor->gpio_fd; - break; - } - } - - uint64_t offset = nanos_since_epoch() - nanos_since_boot(); - struct pollfd fd_list[1] = {0}; - fd_list[0].fd = fd; - fd_list[0].events = POLLIN | POLLPRI; - - while (!do_exit) { - int err = poll(fd_list, 1, 100); - if (err == -1) { - if (errno == EINTR) { - continue; - } - return; - } else if (err == 0) { - LOGE("poll timed out"); - continue; - } - - if ((fd_list[0].revents & (POLLIN | POLLPRI)) == 0) { - LOGE("no poll events set"); - continue; - } - - // Read all events - struct gpioevent_data evdata[16]; - err = HANDLE_EINTR(read(fd, evdata, sizeof(evdata))); - if (err < 0 || err % sizeof(*evdata) != 0) { - LOGE("error reading event data %d", err); - continue; - } - - uint64_t cur_offset = nanos_since_epoch() - nanos_since_boot(); - uint64_t diff = cur_offset > offset ? cur_offset - offset : offset - cur_offset; - if (diff > 10*1e6) { // 10ms - LOGW("time jumped: %lu %lu", cur_offset, offset); - offset = cur_offset; - - // we don't have a valid timestamp since the - // time jumped, so throw out this measurement. - continue; - } - - int num_events = err / sizeof(*evdata); - uint64_t ts = evdata[num_events - 1].timestamp - cur_offset; - - for (auto &[sensor, msg_name] : sensors) { - if (!sensor->has_interrupt_enabled()) { - continue; - } - - MessageBuilder msg; - if (!sensor->get_event(msg, ts)) { - continue; - } - - if (!sensor->is_data_valid(ts)) { - continue; - } - - pm.send(msg_name.c_str(), msg); - } - } -} - -void polling_loop(Sensor *sensor, std::string msg_name) { - PubMaster pm({msg_name.c_str()}); - RateKeeper rk(msg_name, services.at(msg_name).frequency); - while (!do_exit) { - MessageBuilder msg; - if (sensor->get_event(msg) && sensor->is_data_valid(nanos_since_boot())) { - pm.send(msg_name.c_str(), msg); - } - rk.keepTime(); - } -} - -int sensor_loop(I2CBus *i2c_bus_imu) { - // Sensor init - std::vector> sensors_init = { - {new BMX055_Accel(i2c_bus_imu), "accelerometer2"}, - {new BMX055_Gyro(i2c_bus_imu), "gyroscope2"}, - {new BMX055_Magn(i2c_bus_imu), "magnetometer"}, - {new BMX055_Temp(i2c_bus_imu), "temperatureSensor2"}, - - {new LSM6DS3_Accel(i2c_bus_imu, GPIO_LSM_INT), "accelerometer"}, - {new LSM6DS3_Gyro(i2c_bus_imu, GPIO_LSM_INT, true), "gyroscope"}, - {new LSM6DS3_Temp(i2c_bus_imu), "temperatureSensor"}, - - {new MMC5603NJ_Magn(i2c_bus_imu), "magnetometer"}, - }; - - // Initialize sensors - std::vector threads; - for (auto &[sensor, msg_name] : sensors_init) { - int err = sensor->init(); - if (err < 0) { - continue; - } - - if (!sensor->has_interrupt_enabled()) { - threads.emplace_back(polling_loop, sensor, msg_name); - } - } - - // increase interrupt quality by pinning interrupt and process to core 1 - setpriority(PRIO_PROCESS, 0, -18); - util::set_core_affinity({1}); - - // TODO: get the IRQ number from gpiochip - std::string irq_path = "/proc/irq/336/smp_affinity_list"; - if (!util::file_exists(irq_path)) { - irq_path = "/proc/irq/335/smp_affinity_list"; - } - std::system(util::string_format("sudo su -c 'echo 1 > %s'", irq_path.c_str()).c_str()); - - // thread for reading events via interrupts - threads.emplace_back(&interrupt_loop, std::ref(sensors_init)); - - // wait for all threads to finish - for (auto &t : threads) { - t.join(); - } - - for (auto &[sensor, msg_name] : sensors_init) { - sensor->shutdown(); - delete sensor; - } - return 0; -} - -int main(int argc, char *argv[]) { - try { - auto i2c_bus_imu = std::make_unique(I2C_BUS_IMU); - return sensor_loop(i2c_bus_imu.get()); - } catch (std::exception &e) { - LOGE("I2CBus init failed"); - return -1; - } -} diff --git a/system/sensord/tests/test_sensord.py b/system/sensord/tests/test_sensord.py index 15f1f2dc35..1dab652386 100644 --- a/system/sensord/tests/test_sensord.py +++ b/system/sensord/tests/test_sensord.py @@ -12,13 +12,6 @@ from openpilot.common.timeout import Timeout from openpilot.system.hardware import HARDWARE from openpilot.system.manager.process_config import managed_processes -BMX = { - ('bmx055', 'acceleration'), - ('bmx055', 'gyroUncalibrated'), - ('bmx055', 'magneticUncalibrated'), - ('bmx055', 'temperature'), -} - LSM = { ('lsm6ds3', 'acceleration'), ('lsm6ds3', 'gyroUncalibrated'), @@ -30,17 +23,11 @@ MMC = { ('mmc5603nj', 'magneticUncalibrated'), } -SENSOR_CONFIGURATIONS: list[set] = [ - BMX | LSM, - MMC | LSM, - BMX | LSM_C, - MMC| LSM_C, -] -if HARDWARE.get_device_type() == "mici": - SENSOR_CONFIGURATIONS = [ - LSM, - LSM_C, - ] +SENSOR_CONFIGURATIONS: list[set] = { + "mici": [LSM, LSM_C], + "tizi": [MMC | LSM, MMC | LSM_C], + "tici": [LSM, LSM_C, MMC | LSM, MMC | LSM_C], +}.get(HARDWARE.get_device_type(), []) Sensor = log.SensorEventData.SensorSource SensorConfig = namedtuple('SensorConfig', ['type', 'sanity_min', 'sanity_max']) @@ -57,13 +44,6 @@ ALL_SENSORS = { SensorConfig("temperature", 0, 60), }, - Sensor.bmx055: { - SensorConfig("acceleration", 5, 15), - SensorConfig("gyroUncalibrated", 0, .2), - SensorConfig("magneticUncalibrated", 0, 300), - SensorConfig("temperature", 0, 60), - }, - Sensor.mmc5603nj: { SensorConfig("magneticUncalibrated", 0, 300), } diff --git a/system/sensord/tests/ttff_test.py b/system/sensord/tests/ttff_test.py deleted file mode 100755 index a9cc16d707..0000000000 --- a/system/sensord/tests/ttff_test.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 - -import time -import atexit - -from cereal import messaging -from openpilot.system.manager.process_config import managed_processes - -TIMEOUT = 10*60 - -def kill(): - for proc in ['ubloxd', 'pigeond']: - managed_processes[proc].stop(retry=True, block=True) - -if __name__ == "__main__": - # start ubloxd - managed_processes['ubloxd'].start() - atexit.register(kill) - - sm = messaging.SubMaster(['ubloxGnss']) - - times = [] - for i in range(20): - # start pigeond - st = time.monotonic() - managed_processes['pigeond'].start() - - # wait for a >4 satellite fix - while True: - sm.update(0) - msg = sm['ubloxGnss'] - if msg.which() == 'measurementReport' and sm.updated["ubloxGnss"]: - report = msg.measurementReport - if report.numMeas > 4: - times.append(time.monotonic() - st) - print(f"\033[94m{i}: Got a fix in {round(times[-1], 2)} seconds\033[0m") - break - - if time.monotonic() - st > TIMEOUT: - raise TimeoutError("\033[91mFailed to get a fix in {TIMEOUT} seconds!\033[0m") - - time.sleep(0.1) - - # stop pigeond - managed_processes['pigeond'].stop(retry=True, block=True) - time.sleep(20) - - print(f"\033[92mAverage TTFF: {round(sum(times) / len(times), 2)}s\033[0m") diff --git a/system/ui/README.md b/system/ui/README.md index c71b77ab66..85d32bfd6c 100644 --- a/system/ui/README.md +++ b/system/ui/README.md @@ -3,7 +3,7 @@ The user interfaces here are built with [raylib](https://www.raylib.com/). Quick start: -* set `DEBUG_FPS=1` to show the FPS +* set `SHOW_FPS=1` to show the FPS * set `STRICT_MODE=1` to kill the app if it drops too much below 60fps * set `SCALE=1.5` to scale the entire UI by 1.5x * https://www.raylib.com/cheatsheet/cheatsheet.html diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 37da413806..805fdcc71f 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -1,7 +1,10 @@ import atexit +import cffi import os import time import pyray as rl +from collections.abc import Callable +from dataclasses import dataclass from enum import IntEnum from importlib.resources import as_file, files from openpilot.common.swaglog import cloudlog @@ -12,8 +15,8 @@ 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 -ENABLE_VSYNC = os.getenv("ENABLE_VSYNC") == "1" -DEBUG_FPS = os.getenv("DEBUG_FPS") == '1' +ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "1") == "1" +SHOW_FPS = os.getenv("SHOW_FPS") == '1' STRICT_MODE = os.getenv("STRICT_MODE") == '1' SCALE = float(os.getenv("SCALE", "1.0")) @@ -36,6 +39,12 @@ class FontWeight(IntEnum): BLACK = 8 +@dataclass +class ModalOverlay: + overlay: object = None + callback: Callable | None = None + + class GuiApplication: def __init__(self, width: int, height: int): self._fonts: dict[FontWeight, rl.Font] = {} @@ -50,6 +59,7 @@ class GuiApplication: self._last_fps_log_time: float = time.monotonic() self._window_close_requested = False self._trace_log_callback = None + self._modal_overlay = ModalOverlay() def request_close(self): self._window_close_requested = True @@ -79,6 +89,9 @@ class GuiApplication: self._set_styles() self._load_fonts() + def set_modal_overlay(self, overlay, callback: Callable | None = None): + self._modal_overlay = ModalOverlay(overlay=overlay, callback=callback) + def texture(self, asset_path: str, width: int, height: int, alpha_premultiply=False, keep_aspect_ratio=True): cache_key = f"{asset_path}_{width}_{height}_{alpha_premultiply}{keep_aspect_ratio}" if cache_key in self._textures: @@ -148,7 +161,23 @@ class GuiApplication: rl.begin_drawing() rl.clear_background(rl.BLACK) - yield + # Handle modal overlay rendering and input processing + if self._modal_overlay.overlay: + if hasattr(self._modal_overlay.overlay, 'render'): + result = self._modal_overlay.overlay.render(rl.Rectangle(0, 0, self.width, self.height)) + elif callable(self._modal_overlay.overlay): + result = self._modal_overlay.overlay() + else: + raise Exception + + if result >= 0: + # Execute callback with the result and clear the overlay + if self._modal_overlay.callback is not None: + self._modal_overlay.callback(result) + + self._modal_overlay = ModalOverlay() + else: + yield if self._render_texture: rl.end_texture_mode() @@ -158,7 +187,7 @@ class GuiApplication: dst_rect = rl.Rectangle(0, 0, float(self._scaled_width), float(self._scaled_height)) rl.draw_texture_pro(self._render_texture.texture, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE) - if DEBUG_FPS: + if SHOW_FPS: rl.draw_fps(10, 10) rl.end_drawing() @@ -196,13 +225,14 @@ class GuiApplication: for layout in KEYBOARD_LAYOUTS.values(): all_chars.update(key for row in layout for key in row) all_chars = "".join(all_chars) + all_chars += "–✓°" codepoint_count = rl.ffi.new("int *", 1) codepoints = rl.load_codepoints(all_chars, codepoint_count) for index, font_file in enumerate(font_files): with as_file(FONT_DIR.joinpath(font_file)) as fspath: - font = rl.load_font_ex(fspath.as_posix(), 120, codepoints, codepoint_count[0]) + font = rl.load_font_ex(fspath.as_posix(), 200, codepoints, codepoint_count[0]) rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) self._fonts[index] = font @@ -217,12 +247,29 @@ class GuiApplication: rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255))) def _set_log_callback(self): + ffi_libc = cffi.FFI() + ffi_libc.cdef(""" + int vasprintf(char **strp, const char *fmt, void *ap); + void free(void *ptr); + """) + libc = ffi_libc.dlopen(None) + @rl.ffi.callback("void(int, char *, void *)") def trace_log_callback(log_level, text, args): try: - text_str = rl.ffi.string(text).decode('utf-8') - except (TypeError, UnicodeDecodeError): - text_str = str(text) + text_addr = int(rl.ffi.cast("uintptr_t", text)) + args_addr = int(rl.ffi.cast("uintptr_t", args)) + text_libc = ffi_libc.cast("char *", text_addr) + args_libc = ffi_libc.cast("void *", args_addr) + + out = ffi_libc.new("char **") + if libc.vasprintf(out, text_libc, args_libc) >= 0 and out[0] != ffi_libc.NULL: + text_str = ffi_libc.string(out[0]).decode("utf-8", "replace") + libc.free(out[0]) + else: + text_str = rl.ffi.string(text).decode("utf-8", "replace") + except Exception as e: + text_str = f"[Log decode error: {e}]" if log_level == rl.TraceLogLevel.LOG_ERROR: cloudlog.error(f"raylib: {text_str}") diff --git a/system/ui/lib/button.py b/system/ui/lib/button.py index 9ca82c9732..121e7f86fa 100644 --- a/system/ui/lib/button.py +++ b/system/ui/lib/button.py @@ -1,6 +1,7 @@ import pyray as rl from enum import IntEnum from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached class ButtonStyle(IntEnum): @@ -9,6 +10,7 @@ class ButtonStyle(IntEnum): DANGER = 2 # For critical actions, like reboot or delete TRANSPARENT = 3 # For buttons with transparent background and border ACTION = 4 + LIST_ACTION = 5 # For list items with action buttons class TextAlignment(IntEnum): @@ -19,11 +21,17 @@ class TextAlignment(IntEnum): ICON_PADDING = 15 DEFAULT_BUTTON_FONT_SIZE = 60 -BUTTON_ENABLED_TEXT_COLOR = rl.Color(228, 228, 228, 255) BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51) ACTION_BUTTON_FONT_SIZE = 48 -ACTION_BUTTON_TEXT_COLOR = rl.Color(0, 0, 0, 255) +BUTTON_TEXT_COLOR = { + ButtonStyle.NORMAL: rl.Color(228, 228, 228, 255), + ButtonStyle.PRIMARY: rl.Color(228, 228, 228, 255), + ButtonStyle.DANGER: rl.Color(228, 228, 228, 255), + ButtonStyle.TRANSPARENT: rl.BLACK, + ButtonStyle.ACTION: rl.Color(0, 0, 0, 255), + ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255), +} BUTTON_BACKGROUND_COLORS = { ButtonStyle.NORMAL: rl.Color(51, 51, 51, 255), @@ -31,6 +39,7 @@ BUTTON_BACKGROUND_COLORS = { ButtonStyle.DANGER: rl.Color(255, 36, 36, 255), ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.ACTION: rl.Color(189, 189, 189, 255), + ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255), } BUTTON_PRESSED_BACKGROUND_COLORS = { @@ -39,6 +48,7 @@ BUTTON_PRESSED_BACKGROUND_COLORS = { ButtonStyle.DANGER: rl.Color(255, 36, 36, 255), ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.ACTION: rl.Color(130, 130, 130, 255), + ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74), } _pressed_buttons: set[str] = set() # Track mouse press state globally @@ -99,7 +109,7 @@ def gui_button( # Handle icon and text positioning font = gui_app.font(font_weight) - text_size = rl.measure_text_ex(font, text, font_size, 0) + text_size = measure_text_cached(font, text, font_size) text_pos = rl.Vector2(0, rect.y + (rect.height - text_size.y) // 2) # Vertical centering # Draw icon if provided @@ -132,7 +142,7 @@ def gui_button( # Draw the button text if any if text: - text_color = ACTION_BUTTON_TEXT_COLOR if button_style == ButtonStyle.ACTION else BUTTON_ENABLED_TEXT_COLOR if is_enabled else BUTTON_DISABLED_TEXT_COLOR - rl.draw_text_ex(font, text, text_pos, font_size, 0, text_color) + color = BUTTON_TEXT_COLOR[button_style] if is_enabled else BUTTON_DISABLED_TEXT_COLOR + rl.draw_text_ex(font, text, text_pos, font_size, 0, color) return result diff --git a/system/ui/lib/egl.py b/system/ui/lib/egl.py index d43be482b3..d119a8a832 100644 --- a/system/ui/lib/egl.py +++ b/system/ui/lib/egl.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from typing import Any from openpilot.common.swaglog import cloudlog - # EGL constants EGL_LINUX_DMA_BUF_EXT = 0x3270 EGL_WIDTH = 0x3057 @@ -23,6 +22,7 @@ GL_TEXTURE_EXTERNAL_OES = 0x8D65 # DRM Format for NV12 DRM_FORMAT_NV12 = 842094158 + @dataclass class EGLImage: """Container for EGL image and associated resources""" diff --git a/system/ui/lib/inputbox.py b/system/ui/lib/inputbox.py index 367e0dc7e4..ccf718ba56 100644 --- a/system/ui/lib/inputbox.py +++ b/system/ui/lib/inputbox.py @@ -1,14 +1,16 @@ import pyray as rl import time from openpilot.system.ui.lib.application import gui_app - +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.widget import Widget PASSWORD_MASK_CHAR = "•" PASSWORD_MASK_DELAY = 1.5 # Seconds to show character before masking -class InputBox: +class InputBox(Widget): def __init__(self, max_text_size=255, password_mode=False): + super().__init__() self._max_text_size = max_text_size self._input_text = "" self._cursor_position = 0 @@ -22,7 +24,7 @@ class InputBox: self._text_offset = 0 self._visible_width = 0 self._last_char_time = 0 # Track when last character was added - self._masked_length = 0 # How many characters are currently masked + self._masked_length = 0 # How many characters are currently masked @property def text(self): @@ -60,7 +62,7 @@ class InputBox: padding = 10 if self._cursor_position > 0: - cursor_x = rl.measure_text_ex(font, display_text[: self._cursor_position], self._font_size, 0).x + cursor_x = measure_text_cached(font, display_text[: self._cursor_position], self._font_size).x else: cursor_x = 0 @@ -75,11 +77,11 @@ class InputBox: def add_char_at_cursor(self, char): """Add a character at the current cursor position.""" if len(self._input_text) < self._max_text_size: - self._input_text = self._input_text[: self._cursor_position] + char + self._input_text[self._cursor_position :] + self._input_text = self._input_text[: self._cursor_position] + char + self._input_text[self._cursor_position:] self.set_cursor_position(self._cursor_position + 1) if self._password_mode: - self._last_char_time = time.time() + self._last_char_time = time.monotonic() return True return False @@ -87,7 +89,7 @@ class InputBox: def delete_char_before_cursor(self): """Delete the character before the cursor position (backspace).""" if self._cursor_position > 0: - self._input_text = self._input_text[: self._cursor_position - 1] + self._input_text[self._cursor_position :] + self._input_text = self._input_text[: self._cursor_position - 1] + self._input_text[self._cursor_position:] self.set_cursor_position(self._cursor_position - 1) return True return False @@ -95,12 +97,12 @@ class InputBox: def delete_char_at_cursor(self): """Delete the character at the cursor position (delete).""" if self._cursor_position < len(self._input_text): - self._input_text = self._input_text[: self._cursor_position] + self._input_text[self._cursor_position + 1 :] + self._input_text = self._input_text[: self._cursor_position] + self._input_text[self._cursor_position + 1:] self.set_cursor_position(self._cursor_position) return True return False - def render(self, rect, color=rl.BLACK, border_color=rl.DARKGRAY, text_color=rl.WHITE, font_size=80): + def _render(self, rect, color=rl.BLACK, border_color=rl.DARKGRAY, text_color=rl.WHITE, font_size=80): # Store dimensions for text offset calculations self._visible_width = rect.width self._font_size = font_size @@ -141,7 +143,7 @@ class InputBox: if self._show_cursor: cursor_x = rect.x + padding if len(display_text) > 0 and self._cursor_position > 0: - cursor_x += rl.measure_text_ex(font, display_text[: self._cursor_position], font_size, 0).x + cursor_x += measure_text_cached(font, display_text[: self._cursor_position], font_size).x # Apply text offset to cursor position cursor_x -= self._text_offset @@ -159,18 +161,18 @@ class InputBox: # Show character at last edited position if within delay window masked_text = PASSWORD_MASK_CHAR * len(self._input_text) - recent_edit = time.time() - self._last_char_time < PASSWORD_MASK_DELAY + recent_edit = time.monotonic() - self._last_char_time < PASSWORD_MASK_DELAY if recent_edit and self._input_text: last_pos = max(0, self._cursor_position - 1) if last_pos < len(self._input_text): - return masked_text[:last_pos] + self._input_text[last_pos] + masked_text[last_pos + 1 :] + return masked_text[:last_pos] + self._input_text[last_pos] + masked_text[last_pos + 1:] 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.MOUSE_LEFT_BUTTON) and rl.check_collision_point_rec(mouse_pos, rect): + 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() @@ -182,7 +184,7 @@ class InputBox: min_distance = float('inf') for i in range(len(self._input_text) + 1): - char_width = rl.measure_text_ex(font, display_text[:i], font_size, 0).x + 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 diff --git a/system/ui/lib/label.py b/system/ui/lib/label.py index 5244c6baf2..c3d0e0303a 100644 --- a/system/ui/lib/label.py +++ b/system/ui/lib/label.py @@ -1,5 +1,6 @@ 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 @@ -14,7 +15,7 @@ def gui_label( elide_right: bool = True ): font = gui_app.font(font_weight) - text_size = rl.measure_text_ex(font, text, font_size, 0) + text_size = measure_text_cached(font, text, font_size) display_text = text # Elide text to fit within the rectangle @@ -24,13 +25,13 @@ def gui_label( while left < right: mid = (left + right) // 2 candidate = text[:mid] + ellipsis - candidate_size = rl.measure_text_ex(font, candidate, font_size, 0) + candidate_size = measure_text_cached(font, candidate, font_size) if candidate_size.x <= rect.width: left = mid + 1 else: right = mid display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis - text_size = rl.measure_text_ex(font, display_text, font_size, 0) + text_size = measure_text_cached(font, display_text, font_size) # Calculate horizontal position based on alignment text_x = rect.x + { @@ -56,7 +57,8 @@ def gui_text_box( font_size: int = DEFAULT_TEXT_SIZE, color: rl.Color = DEFAULT_TEXT_COLOR, alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP + alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, + font_weight: FontWeight = FontWeight.NORMAL, ): styles = [ (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)), @@ -66,6 +68,11 @@ def gui_text_box( (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical), (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD) ] + if font_weight != FontWeight.NORMAL: + rl.gui_set_font(gui_app.font(font_weight)) with GuiStyleContext(styles): rl.gui_label(rect, text) + + if font_weight != FontWeight.NORMAL: + rl.gui_set_font(gui_app.font(FontWeight.NORMAL)) diff --git a/system/ui/lib/list_view.py b/system/ui/lib/list_view.py new file mode 100644 index 0000000000..f5581be23e --- /dev/null +++ b/system/ui/lib/list_view.py @@ -0,0 +1,368 @@ +import os +import pyray as rl +from collections.abc import Callable +from abc import ABC +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.lib.button import gui_button, ButtonStyle +from openpilot.system.ui.lib.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT +from openpilot.system.ui.lib.widget import Widget + +ITEM_BASE_WIDTH = 600 +ITEM_BASE_HEIGHT = 170 +ITEM_PADDING = 20 +ITEM_TEXT_FONT_SIZE = 50 +ITEM_TEXT_COLOR = rl.WHITE +ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255) +ITEM_DESC_FONT_SIZE = 40 +ITEM_DESC_V_OFFSET = 140 +RIGHT_ITEM_PADDING = 20 +ICON_SIZE = 80 +BUTTON_WIDTH = 250 +BUTTON_HEIGHT = 100 +BUTTON_BORDER_RADIUS = 50 +BUTTON_FONT_SIZE = 35 +BUTTON_FONT_WEIGHT = FontWeight.MEDIUM + +TEXT_PADDING = 20 + + +def _resolve_value(value, default=""): + if callable(value): + return value() + return value if value is not None else default + + +# Abstract base class for right-side items +class ItemAction(Widget, ABC): + def __init__(self, width: int = BUTTON_HEIGHT, enabled: bool | Callable[[], bool] = True): + super().__init__() + self.set_rect(rl.Rectangle(0, 0, width, 0)) + self._enabled_source = enabled + + @property + def enabled(self): + return _resolve_value(self._enabled_source, False) + + +class ToggleAction(ItemAction): + def __init__(self, initial_state: bool = False, width: int = TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True): + super().__init__(width, enabled) + self.toggle = Toggle(initial_state=initial_state) + self.state = initial_state + + def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: + super().set_touch_valid_callback(touch_callback) + self.toggle.set_touch_valid_callback(touch_callback) + + def _render(self, rect: rl.Rectangle) -> bool: + self.toggle.set_enabled(self.enabled) + self.toggle.render(rl.Rectangle(rect.x, rect.y + (rect.height - TOGGLE_HEIGHT) / 2, self._rect.width, TOGGLE_HEIGHT)) + return False + + def set_state(self, state: bool): + self.state = state + self.toggle.set_state(state) + + def get_state(self) -> bool: + return self.state + + +class ButtonAction(ItemAction): + def __init__(self, text: str | Callable[[], str], width: int = BUTTON_WIDTH, enabled: bool | Callable[[], bool] = True): + super().__init__(width, enabled) + self._text_source = text + + @property + def text(self): + return _resolve_value(self._text_source, "Error") + + def _render(self, rect: rl.Rectangle) -> bool: + return gui_button( + rl.Rectangle(rect.x, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT), + self.text, + border_radius=BUTTON_BORDER_RADIUS, + font_weight=BUTTON_FONT_WEIGHT, + font_size=BUTTON_FONT_SIZE, + button_style=ButtonStyle.LIST_ACTION, + is_enabled=self.enabled, + ) == 1 + + +class TextAction(ItemAction): + def __init__(self, text: str | Callable[[], str], color: rl.Color = ITEM_TEXT_COLOR, enabled: bool | Callable[[], bool] = True): + self._text_source = text + self.color = color + + self._font = gui_app.font(FontWeight.NORMAL) + initial_text = _resolve_value(text, "") + text_width = measure_text_cached(self._font, initial_text, ITEM_TEXT_FONT_SIZE).x + super().__init__(int(text_width + TEXT_PADDING), enabled) + + @property + def text(self): + return _resolve_value(self._text_source, "Error") + + def _render(self, rect: rl.Rectangle) -> bool: + current_text = self.text + text_size = measure_text_cached(self._font, current_text, ITEM_TEXT_FONT_SIZE) + + text_x = rect.x + (rect.width - text_size.x) / 2 + text_y = rect.y + (rect.height - text_size.y) / 2 + rl.draw_text_ex(self._font, current_text, rl.Vector2(text_x, text_y), ITEM_TEXT_FONT_SIZE, 0, self.color) + return False + + def get_width(self) -> int: + text_width = measure_text_cached(self._font, self.text, ITEM_TEXT_FONT_SIZE).x + return int(text_width + TEXT_PADDING) + + +class DualButtonAction(ItemAction): + def __init__(self, left_text: str, right_text: str, left_callback: Callable = None, + right_callback: Callable = None, enabled: bool | Callable[[], bool] = True): + super().__init__(width=0, enabled=enabled) # Width 0 means use full width + self.left_text, self.right_text = left_text, right_text + self.left_callback, self.right_callback = left_callback, right_callback + + def _render(self, rect: rl.Rectangle) -> bool: + button_spacing = 30 + button_height = 120 + button_width = (rect.width - button_spacing) / 2 + button_y = rect.y + (rect.height - button_height) / 2 + + left_rect = rl.Rectangle(rect.x, button_y, button_width, button_height) + right_rect = rl.Rectangle(rect.x + button_width + button_spacing, button_y, button_width, button_height) + + left_clicked = gui_button(left_rect, self.left_text, button_style=ButtonStyle.LIST_ACTION) == 1 + right_clicked = gui_button(right_rect, self.right_text, button_style=ButtonStyle.DANGER) == 1 + + if left_clicked and self.left_callback: + self.left_callback() + return True + if right_clicked and self.right_callback: + self.right_callback() + return True + return False + + +class MultipleButtonAction(ItemAction): + def __init__(self, buttons: list[str], button_width: int, selected_index: int = 0, callback: Callable = None): + super().__init__(width=len(buttons) * (button_width + 20), enabled=True) + self.buttons = buttons + self.button_width = button_width + self.selected_button = selected_index + self.callback = callback + self._font = gui_app.font(FontWeight.MEDIUM) + + def _render(self, rect: rl.Rectangle) -> bool: + spacing = 20 + button_y = rect.y + (rect.height - BUTTON_HEIGHT) / 2 + clicked = -1 + + for i, text in enumerate(self.buttons): + button_x = rect.x + i * (self.button_width + spacing) + button_rect = rl.Rectangle(button_x, button_y, self.button_width, BUTTON_HEIGHT) + + # 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_selected = i == self.selected_button + + # Button colors + if is_selected: + bg_color = rl.Color(51, 171, 76, 255) # Green + elif is_pressed: + bg_color = rl.Color(74, 74, 74, 255) # Dark gray + else: + bg_color = rl.Color(57, 57, 57, 255) # Gray + + # Draw button + rl.draw_rectangle_rounded(button_rect, 1.0, 20, bg_color) + + # Draw text + text_size = measure_text_cached(self._font, text, 40) + text_x = button_x + (self.button_width - text_size.x) / 2 + text_y = button_y + (BUTTON_HEIGHT - text_size.y) / 2 + 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: + clicked = i + + if clicked >= 0: + self.selected_button = clicked + if self.callback: + self.callback(clicked) + return True + return False + + +class ListItem(Widget): + def __init__(self, title: str = "", icon: str | None = None, description: str | Callable[[], str] | None = None, + description_visible: bool = False, callback: Callable | None = None, + action_item: ItemAction | None = None): + super().__init__() + self.title = title + self.icon = icon + self.description = description + self.description_visible = description_visible + self.callback = callback + self.action_item = action_item + + self.set_rect(rl.Rectangle(0, 0, ITEM_BASE_WIDTH, ITEM_BASE_HEIGHT)) + self._font = gui_app.font(FontWeight.NORMAL) + + # Cached properties for performance + self._prev_max_width: int = 0 + self._wrapped_description: str | None = None + self._prev_description: str | None = None + self._description_height: float = 0 + + def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: + super().set_touch_valid_callback(touch_callback) + if self.action_item: + self.action_item.set_touch_valid_callback(touch_callback) + + def set_parent_rect(self, parent_rect: rl.Rectangle): + super().set_parent_rect(parent_rect) + self._rect.width = parent_rect.width + + def _handle_mouse_release(self, mouse_pos: rl.Vector2): + if not self.is_visible: + return + + # Check not in action rect + if self.action_item: + action_rect = self.get_right_item_rect(self._rect) + if rl.check_collision_point_rec(mouse_pos, action_rect): + # Click was on right item, don't toggle description + return + + if self.description: + self.description_visible = not self.description_visible + content_width = self.get_content_width(int(self._rect.width - ITEM_PADDING * 2)) + self._rect.height = self.get_item_height(self._font, content_width) + + def _render(self, _): + if not self.is_visible: + return + + # Don't draw items that are not in parent's viewport + if ((self._rect.y + self.rect.height) <= self._parent_rect.y or + self._rect.y >= (self._parent_rect.y + self._parent_rect.height)): + return + + content_x = self._rect.x + ITEM_PADDING + text_x = content_x + + # Only draw title and icon for items that have them + if self.title: + # Draw icon if present + if self.icon: + icon_texture = gui_app.texture(os.path.join("icons", self.icon), ICON_SIZE, ICON_SIZE) + rl.draw_texture(icon_texture, int(content_x), int(self._rect.y + (ITEM_BASE_HEIGHT - icon_texture.width) // 2), rl.WHITE) + text_x += ICON_SIZE + ITEM_PADDING + + # Draw main text + text_size = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE) + item_y = self._rect.y + (ITEM_BASE_HEIGHT - text_size.y) // 2 + rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR) + + # Draw description if visible + current_description = self.get_description() + if self.description_visible and current_description and self._wrapped_description: + rl.draw_text_ex( + self._font, + self._wrapped_description, + rl.Vector2(text_x, self._rect.y + ITEM_DESC_V_OFFSET), + ITEM_DESC_FONT_SIZE, + 0, + ITEM_DESC_TEXT_COLOR, + ) + + # Draw right item if present + if self.action_item: + right_rect = self.get_right_item_rect(self._rect) + right_rect.y = self._rect.y + if self.action_item.render(right_rect) and self.action_item.enabled: + # Right item was clicked/activated + if self.callback: + self.callback() + + def get_description(self): + return _resolve_value(self.description, None) + + def get_item_height(self, font: rl.Font, max_width: int) -> float: + if not self.is_visible: + return 0 + + current_description = self.get_description() + if self.description_visible and current_description: + if ( + not self._wrapped_description + or current_description != self._prev_description + or max_width != self._prev_max_width + ): + self._prev_max_width = max_width + self._prev_description = current_description + + wrapped_lines = wrap_text(font, current_description, ITEM_DESC_FONT_SIZE, max_width) + self._wrapped_description = "\n".join(wrapped_lines) + self._description_height = len(wrapped_lines) * ITEM_DESC_FONT_SIZE + 10 + return ITEM_BASE_HEIGHT + self._description_height - (ITEM_BASE_HEIGHT - ITEM_DESC_V_OFFSET) + ITEM_PADDING + return ITEM_BASE_HEIGHT + + def get_content_width(self, total_width: int) -> int: + if self.action_item and self.action_item.rect.width > 0: + return total_width - int(self.action_item.rect.width) - RIGHT_ITEM_PADDING + return total_width + + def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle: + if not self.action_item: + return rl.Rectangle(0, 0, 0, 0) + + right_width = self.action_item.rect.width + if right_width == 0: # Full width action (like DualButtonAction) + return rl.Rectangle(item_rect.x + ITEM_PADDING, item_rect.y, + item_rect.width - (ITEM_PADDING * 2), ITEM_BASE_HEIGHT) + + right_x = item_rect.x + item_rect.width - right_width + right_y = item_rect.y + return rl.Rectangle(right_x, right_y, right_width, ITEM_BASE_HEIGHT) + + +# Factory functions +def simple_item(title: str, callback: Callable | None = None) -> ListItem: + return ListItem(title=title, callback=callback) + + +def toggle_item(title: str, description: str | Callable[[], str] | None = None, initial_state: bool = False, + callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True) -> ListItem: + action = ToggleAction(initial_state=initial_state, enabled=enabled) + return ListItem(title=title, description=description, action_item=action, icon=icon, callback=callback) + + +def button_item(title: str, button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None, + callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem: + action = ButtonAction(text=button_text, enabled=enabled) + return ListItem(title=title, description=description, action_item=action, callback=callback) + + +def text_item(title: str, value: str | Callable[[], str], description: str | Callable[[], str] | None = None, + callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem: + action = TextAction(text=value, color=rl.Color(170, 170, 170, 255), enabled=enabled) + return ListItem(title=title, description=description, action_item=action, callback=callback) + + +def dual_button_item(left_text: str, right_text: str, left_callback: Callable = None, right_callback: Callable = None, + description: str | Callable[[], str] | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem: + action = DualButtonAction(left_text, right_text, left_callback, right_callback, enabled) + return ListItem(title="", description=description, action_item=action) + + +def multiple_button_item(title: str, description: str, buttons: list[str], selected_index: int, + button_width: int = BUTTON_WIDTH, callback: Callable = None, icon: str = ""): + action = MultipleButtonAction(buttons, button_width, selected_index, callback=callback) + return ListItem(title=title, description=description, icon=icon, action_item=action) diff --git a/system/ui/lib/scroll_panel.py b/system/ui/lib/scroll_panel.py index 43111504bb..32f3b7b575 100644 --- a/system/ui/lib/scroll_panel.py +++ b/system/ui/lib/scroll_panel.py @@ -1,11 +1,12 @@ import pyray as rl +from collections import deque from enum import IntEnum # Scroll constants for smooth scrolling behavior MOUSE_WHEEL_SCROLL_SPEED = 30 INERTIA_FRICTION = 0.92 # The rate at which the inertia slows down MIN_VELOCITY = 0.5 # Minimum velocity before stopping the inertia -DRAG_THRESHOLD = 5 # Pixels of movement to consider it a drag, not a click +DRAG_THRESHOLD = 12 # Pixels of movement to consider it a drag, not a click BOUNCE_FACTOR = 0.2 # Elastic bounce when scrolling past boundaries BOUNCE_RETURN_SPEED = 0.15 # How quickly it returns from the bounce MAX_BOUNCE_DISTANCE = 150 # Maximum distance for bounce effect @@ -31,8 +32,7 @@ class GuiScrollPanel: self._velocity_y = 0.0 # Velocity for inertia self._is_dragging: bool = False self._bounce_offset: float = 0.0 - self._last_frame_time = rl.get_time() - self._velocity_history: list[float] = [] + self._velocity_history: deque[float] = deque(maxlen=VELOCITY_HISTORY_SIZE) self._last_drag_time: float = 0.0 self._content_rect: rl.Rectangle | None = None self._bounds_rect: rl.Rectangle | None = None @@ -44,11 +44,6 @@ class GuiScrollPanel: # Calculate time delta current_time = rl.get_time() - delta_time = current_time - self._last_frame_time - self._last_frame_time = current_time - - # Prevent large jumps - delta_time = min(delta_time, 0.05) mouse_pos = rl.get_mouse_position() max_scroll_y = max(content.height - bounds.height, 0) @@ -63,13 +58,15 @@ class GuiScrollPanel: if mouse_pos.x >= scrollbar_x: self._scroll_state = ScrollState.DRAGGING_SCROLLBAR + # TODO: hacky + # when clicking while moving, go straight into dragging + self._is_dragging = abs(self._velocity_y) > MIN_VELOCITY self._last_mouse_y = mouse_pos.y self._start_mouse_y = mouse_pos.y self._last_drag_time = current_time - self._velocity_history = [] + self._velocity_history.clear() self._velocity_y = 0.0 self._bounce_offset = 0.0 - self._is_dragging = False # Handle active dragging if self._scroll_state == ScrollState.DRAGGING_CONTENT or self._scroll_state == ScrollState.DRAGGING_SCROLLBAR: @@ -82,9 +79,6 @@ class GuiScrollPanel: drag_velocity = delta_y / time_since_last_drag / 60.0 self._velocity_history.append(drag_velocity) - if len(self._velocity_history) > VELOCITY_HISTORY_SIZE: - self._velocity_history.pop(0) - self._last_drag_time = current_time # Detect actual dragging @@ -175,13 +169,8 @@ class GuiScrollPanel: return self._offset - def is_click_valid(self) -> bool: - # Check if this is a click rather than a drag - return ( - self._scroll_state == ScrollState.IDLE - and not self._is_dragging - and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) - ) + def is_touch_valid(self): + return not self._is_dragging def get_normalized_scroll_position(self) -> float: """Returns the current scroll position as a value from 0.0 to 1.0""" diff --git a/system/ui/lib/scroller.py b/system/ui/lib/scroller.py new file mode 100644 index 0000000000..8a10221772 --- /dev/null +++ b/system/ui/lib/scroller.py @@ -0,0 +1,74 @@ +import pyray as rl +from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel + +ITEM_SPACING = 40 +LINE_COLOR = rl.GRAY +LINE_PADDING = 40 + + +class LineSeparator(Widget): + def __init__(self, height: int = 1): + super().__init__() + self._rect = rl.Rectangle(0, 0, 0, height) + + def set_parent_rect(self, parent_rect: rl.Rectangle) -> None: + super().set_parent_rect(parent_rect) + self._rect.width = parent_rect.width + + def _render(self, _): + rl.draw_line(int(self._rect.x) + LINE_PADDING, int(self._rect.y), + int(self._rect.x + self._rect.width) - LINE_PADDING * 2, int(self._rect.y), + LINE_COLOR) + + +class Scroller(Widget): + def __init__(self, items: list[Widget], spacing: int = ITEM_SPACING, line_separator: bool = False, pad_end: bool = True): + super().__init__() + self._items: list[Widget] = [] + self._spacing = spacing + self._line_separator = line_separator + self._pad_end = pad_end + + self.scroll_panel = GuiScrollPanel() + + for item in items: + self.add_widget(item) + + def add_widget(self, item: Widget) -> None: + if self._line_separator and len(self._items) > 0: + self._items.append(LineSeparator()) + self._items.append(item) + item.set_touch_valid_callback(self.scroll_panel.is_touch_valid) + + def _render(self, _): + # TODO: don't draw items that are not in the viewport + visible_items = [item for item in self._items if item.is_visible] + content_height = sum(item.rect.height for item in visible_items) + self._spacing * (len(visible_items)) + if not self._pad_end: + content_height -= self._spacing + scroll = self.scroll_panel.handle_scroll(self._rect, rl.Rectangle(0, 0, self._rect.width, content_height)) + + rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), + int(self._rect.width), int(self._rect.height)) + + cur_height = 0 + for idx, item in enumerate(visible_items): + if not item.is_visible: + continue + + # Nicely lay out items vertically + x = self._rect.x + y = self._rect.y + cur_height + self._spacing * (idx != 0) + cur_height += item.rect.height + self._spacing * (idx != 0) + + # Consider scroll + x += scroll.x + y += scroll.y + + # Update item state + item.set_position(x, y) + item.set_parent_rect(self._rect) + item.render() + + rl.end_scissor_mode() diff --git a/system/ui/lib/shader_polygon.py b/system/ui/lib/shader_polygon.py new file mode 100644 index 0000000000..cfde81c554 --- /dev/null +++ b/system/ui/lib/shader_polygon.py @@ -0,0 +1,321 @@ +import platform +import pyray as rl +import numpy as np +from typing import Any + +MAX_GRADIENT_COLORS = 15 + +VERSION = """ +#version 300 es +precision highp float; +""" +if platform.system() == "Darwin": + VERSION = """ + #version 330 core + """ + +FRAGMENT_SHADER = VERSION + """ +in vec2 fragTexCoord; +out vec4 finalColor; + +uniform vec2 points[100]; +uniform int pointCount; +uniform vec4 fillColor; +uniform vec2 resolution; + +uniform int useGradient; +uniform vec2 gradientStart; +uniform vec2 gradientEnd; +uniform vec4 gradientColors[15]; +uniform float gradientStops[15]; +uniform int gradientColorCount; + +vec4 getGradientColor(vec2 pos) { + vec2 gradientDir = gradientEnd - gradientStart; + float gradientLength = length(gradientDir); + if (gradientLength < 0.001) return gradientColors[0]; + + vec2 normalizedDir = gradientDir / gradientLength; + float t = clamp(dot(pos - gradientStart, normalizedDir) / gradientLength, 0.0, 1.0); + + if (gradientColorCount <= 1) return gradientColors[0]; + for (int i = 0; i < gradientColorCount - 1; i++) { + if (t >= gradientStops[i] && t <= gradientStops[i+1]) { + float segmentT = (t - gradientStops[i]) / (gradientStops[i+1] - gradientStops[i]); + return mix(gradientColors[i], gradientColors[i+1], segmentT); + } + } + + return gradientColors[gradientColorCount-1]; +} + +bool isPointInsidePolygon(vec2 p) { + if (pointCount < 3) return false; + int crossings = 0; + for (int i = 0, j = pointCount - 1; i < pointCount; j = i++) { + vec2 pi = points[i]; + vec2 pj = points[j]; + if (distance(pi, pj) < 0.001) continue; + if (((pi.y > p.y) != (pj.y > p.y)) && + (p.x < (pj.x - pi.x) * (p.y - pi.y) / (pj.y - pi.y + 0.001) + pi.x)) { + crossings++; + } + } + return (crossings & 1) == 1; +} + +float distanceToEdge(vec2 p) { + float minDist = 1000.0; + + for (int i = 0, j = pointCount - 1; i < pointCount; j = i++) { + vec2 edge0 = points[j]; + vec2 edge1 = points[i]; + + if (distance(edge0, edge1) < 0.0001) continue; + + vec2 v1 = p - edge0; + vec2 v2 = edge1 - edge0; + float l2 = dot(v2, v2); + + if (l2 < 0.0001) { + float dist = length(v1); + minDist = min(minDist, dist); + continue; + } + + float t = clamp(dot(v1, v2) / l2, 0.0, 1.0); + vec2 projection = edge0 + t * v2; + float dist = length(p - projection); + minDist = min(minDist, dist); + } + + return minDist; +} + +void main() { + vec2 pixel = fragTexCoord * resolution; + + // Compute pixel size for anti-aliasing + vec2 pixelGrad = vec2(dFdx(pixel.x), dFdy(pixel.y)); + float pixelSize = length(pixelGrad); + float aaWidth = max(0.5, pixelSize * 1.5); + + bool inside = isPointInsidePolygon(pixel); + if (inside) { + finalColor = useGradient == 1 ? getGradientColor(pixel) : fillColor; + return; + } + + float sd = -distanceToEdge(pixel); + float alpha = smoothstep(-aaWidth, aaWidth, sd); + if (alpha > 0.0){ + vec4 color = useGradient == 1 ? getGradientColor(pixel) : fillColor; + finalColor = vec4(color.rgb, color.a * alpha); + } else { + discard; + } +} +""" + +# Default vertex shader +VERTEX_SHADER = VERSION + """ +in vec3 vertexPosition; +in vec2 vertexTexCoord; +out vec2 fragTexCoord; +uniform mat4 mvp; + +void main() { + fragTexCoord = vertexTexCoord; + gl_Position = mvp * vec4(vertexPosition, 1.0); +} +""" + +UNIFORM_INT = rl.ShaderUniformDataType.SHADER_UNIFORM_INT +UNIFORM_FLOAT = rl.ShaderUniformDataType.SHADER_UNIFORM_FLOAT +UNIFORM_VEC2 = rl.ShaderUniformDataType.SHADER_UNIFORM_VEC2 +UNIFORM_VEC4 = rl.ShaderUniformDataType.SHADER_UNIFORM_VEC4 + + +class ShaderState: + _instance: Any = None + + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self): + if ShaderState._instance is not None: + raise Exception("This class is a singleton. Use get_instance() instead.") + + self.initialized = False + self.shader = None + self.white_texture = None + + # Shader uniform locations + self.locations = { + 'pointCount': None, + 'fillColor': None, + 'resolution': None, + 'points': None, + 'useGradient': None, + 'gradientStart': None, + 'gradientEnd': None, + 'gradientColors': None, + 'gradientStops': None, + 'gradientColorCount': None, + 'mvp': None, + } + + # Pre-allocated FFI objects + self.point_count_ptr = rl.ffi.new("int[]", [0]) + self.resolution_ptr = rl.ffi.new("float[]", [0.0, 0.0]) + self.fill_color_ptr = rl.ffi.new("float[]", [0.0, 0.0, 0.0, 0.0]) + self.use_gradient_ptr = rl.ffi.new("int[]", [0]) + self.gradient_start_ptr = rl.ffi.new("float[]", [0.0, 0.0]) + self.gradient_end_ptr = rl.ffi.new("float[]", [0.0, 0.0]) + self.color_count_ptr = rl.ffi.new("int[]", [0]) + self.gradient_colors_ptr = rl.ffi.new("float[]", MAX_GRADIENT_COLORS * 4) + self.gradient_stops_ptr = rl.ffi.new("float[]", MAX_GRADIENT_COLORS) + + def initialize(self): + if self.initialized: + return + + self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAGMENT_SHADER) + + # Create and cache white texture + white_img = rl.gen_image_color(2, 2, rl.WHITE) + self.white_texture = rl.load_texture_from_image(white_img) + rl.set_texture_filter(self.white_texture, rl.TEXTURE_FILTER_BILINEAR) + rl.unload_image(white_img) + + # Cache all uniform locations + for uniform in self.locations.keys(): + self.locations[uniform] = rl.get_shader_location(self.shader, uniform) + + # Setup default MVP matrix + mvp_ptr = rl.ffi.new("float[16]", [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]) + rl.set_shader_value_matrix(self.shader, self.locations['mvp'], rl.Matrix(*mvp_ptr)) + + self.initialized = True + + def cleanup(self): + if not self.initialized: + return + + if self.white_texture: + rl.unload_texture(self.white_texture) + self.white_texture = None + + if self.shader: + rl.unload_shader(self.shader) + self.shader = None + + self.initialized = False + + +def _configure_shader_color(state, color, gradient, clipped_rect, original_rect): + use_gradient = 1 if gradient else 0 + state.use_gradient_ptr[0] = use_gradient + rl.set_shader_value(state.shader, state.locations['useGradient'], state.use_gradient_ptr, UNIFORM_INT) + + if use_gradient: + start = np.array(gradient['start']) * np.array([original_rect.width, original_rect.height]) + np.array([original_rect.x, original_rect.y]) + end = np.array(gradient['end']) * np.array([original_rect.width, original_rect.height]) + np.array([original_rect.x, original_rect.y]) + start = start - np.array([clipped_rect.x, clipped_rect.y]) + end = end - np.array([clipped_rect.x, clipped_rect.y]) + state.gradient_start_ptr[0:2] = start.astype(np.float32) + state.gradient_end_ptr[0:2] = end.astype(np.float32) + rl.set_shader_value(state.shader, state.locations['gradientStart'], state.gradient_start_ptr, UNIFORM_VEC2) + rl.set_shader_value(state.shader, state.locations['gradientEnd'], state.gradient_end_ptr, UNIFORM_VEC2) + + colors = gradient['colors'] + color_count = min(len(colors), MAX_GRADIENT_COLORS) + state.color_count_ptr[0] = color_count + for i, c in enumerate(colors[:color_count]): + base_idx = i * 4 + state.gradient_colors_ptr[base_idx:base_idx+4] = [c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0] + rl.set_shader_value_v(state.shader, state.locations['gradientColors'], state.gradient_colors_ptr, UNIFORM_VEC4, color_count) + + stops = gradient.get('stops', [i / max(1, color_count - 1) for i in range(color_count)]) + stops = np.clip(stops[:color_count], 0.0, 1.0) + state.gradient_stops_ptr[0:color_count] = stops + rl.set_shader_value_v(state.shader, state.locations['gradientStops'], state.gradient_stops_ptr, UNIFORM_FLOAT, color_count) + rl.set_shader_value(state.shader, state.locations['gradientColorCount'], state.color_count_ptr, UNIFORM_INT) + else: + color = color or rl.WHITE + state.fill_color_ptr[0:4] = [color.r / 255.0, color.g / 255.0, color.b / 255.0, color.a / 255.0] + rl.set_shader_value(state.shader, state.locations['fillColor'], state.fill_color_ptr, UNIFORM_VEC4) + + +def draw_polygon(origin_rect: rl.Rectangle, points: np.ndarray, color=None, gradient=None): + """ + Draw a complex polygon using shader-based even-odd fill rule + + Args: + rect: Rectangle defining the drawing area + points: numpy array of (x,y) points defining the polygon + color: Solid fill color (rl.Color) + gradient: Dict with gradient parameters: + { + 'start': (x1, y1), # Start point (normalized 0-1) + 'end': (x2, y2), # End point (normalized 0-1) + 'colors': [rl.Color], # List of colors at stops + 'stops': [float] # List of positions (0-1) + } + """ + if len(points) < 3: + return + + state = ShaderState.get_instance() + if not state.initialized: + state.initialize() + + # Find bounding box + min_xy = np.min(points, axis=0) + max_xy = np.max(points, axis=0) + clip_x = max(origin_rect.x, min_xy[0]) + clip_y = max(origin_rect.y, min_xy[1]) + clip_right = min(origin_rect.x + origin_rect.width, max_xy[0]) + clip_bottom = min(origin_rect.y + origin_rect.height, max_xy[1]) + + # Check if polygon is completely off-screen + if clip_x >= clip_right or clip_y >= clip_bottom: + return + + clipped_rect = rl.Rectangle(clip_x, clip_y, clip_right - clip_x, clip_bottom - clip_y) + + # Transform points relative to the CLIPPED area + transformed_points = points - np.array([clip_x, clip_y]) + + # Set shader values + state.point_count_ptr[0] = len(transformed_points) + rl.set_shader_value(state.shader, state.locations['pointCount'], state.point_count_ptr, UNIFORM_INT) + + state.resolution_ptr[0:2] = [clipped_rect.width, clipped_rect.height] + rl.set_shader_value(state.shader, state.locations['resolution'], state.resolution_ptr, UNIFORM_VEC2) + + flat_points = np.ascontiguousarray(transformed_points.flatten().astype(np.float32)) + points_ptr = rl.ffi.cast("float *", flat_points.ctypes.data) + rl.set_shader_value_v(state.shader, state.locations['points'], points_ptr, UNIFORM_VEC2, len(transformed_points)) + + _configure_shader_color(state, color, gradient, clipped_rect, origin_rect) + + # Render + rl.begin_shader_mode(state.shader) + rl.draw_texture_pro( + state.white_texture, + rl.Rectangle(0, 0, 2, 2), + clipped_rect, + rl.Vector2(0, 0), + 0.0, + rl.WHITE, + ) + rl.end_shader_mode() + + +def cleanup_shader_resources(): + state = ShaderState.get_instance() + state.cleanup() diff --git a/system/ui/lib/text_measure.py b/system/ui/lib/text_measure.py new file mode 100644 index 0000000000..c172f94251 --- /dev/null +++ b/system/ui/lib/text_measure.py @@ -0,0 +1,14 @@ +import pyray as rl + +_cache: dict[int, rl.Vector2] = {} + + +def measure_text_cached(font: rl.Font, text: str, font_size: int, spacing: int = 0) -> rl.Vector2: + """Caches text measurements to avoid redundant calculations.""" + key = hash((font.texture.id, text, font_size, spacing)) + if key in _cache: + return _cache[key] + + result = rl.measure_text_ex(font, text, font_size, spacing) # noqa: TID251 + _cache[key] = result + return result diff --git a/system/ui/lib/toggle.py b/system/ui/lib/toggle.py index e72faef11c..8ed9a655ec 100644 --- a/system/ui/lib/toggle.py +++ b/system/ui/lib/toggle.py @@ -1,53 +1,72 @@ import pyray as rl +from openpilot.system.ui.lib.widget import Widget -ON_COLOR = rl.GREEN +ON_COLOR = rl.Color(51, 171, 76, 255) OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255) KNOB_COLOR = rl.WHITE +DISABLED_ON_COLOR = rl.Color(0x22, 0x77, 0x22, 255) # Dark green when disabled + on +DISABLED_OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255) +DISABLED_KNOB_COLOR = rl.Color(0x88, 0x88, 0x88, 255) +WIDTH, HEIGHT = 160, 80 BG_HEIGHT = 60 -KNOB_HEIGHT = 80 -WIDTH = 160 +ANIMATION_SPEED = 8.0 -class Toggle: - def __init__(self, x, y, initial_state=False): +class Toggle(Widget): + def __init__(self, initial_state=False): + super().__init__() self._state = initial_state - self._rect = rl.Rectangle(x, y, WIDTH, KNOB_HEIGHT) + self._enabled = True + self._progress = 1.0 if initial_state else 0.0 + self._target = self._progress - def handle_input(self): - if rl.is_mouse_button_pressed(rl.MOUSE_LEFT_BUTTON): - mouse_pos = rl.get_mouse_position() - if rl.check_collision_point_rec(mouse_pos, self._rect): - self._state = not self._state + def set_rect(self, rect: rl.Rectangle): + self._rect = rl.Rectangle(rect.x, rect.y, WIDTH, HEIGHT) + + def _handle_mouse_release(self, mouse_pos: rl.Vector2): + if not self._enabled: + return + + self._state = not self._state + self._target = 1.0 if self._state else 0.0 def get_state(self): return self._state - def render(self): - self._draw_background() - self._draw_knob() + def set_state(self, state: bool): + self._state = state + self._target = 1.0 if state else 0.0 - def _draw_background(self): - bg_rect = rl.Rectangle( - self._rect.x + 5, - self._rect.y + (KNOB_HEIGHT - BG_HEIGHT) / 2, - self._rect.width - 10, - BG_HEIGHT, - ) - rl.draw_rectangle_rounded(bg_rect, 1.0, 10, ON_COLOR if self._state else OFF_COLOR) + def set_enabled(self, enabled: bool): + self._enabled = enabled - def _draw_knob(self): - knob_radius = KNOB_HEIGHT / 2 - knob_x = self._rect.x + knob_radius if not self._state else self._rect.x + self._rect.width - knob_radius - knob_y = self._rect.y + knob_radius - rl.draw_circle(int(knob_x), int(knob_y), knob_radius, KNOB_COLOR) + def is_enabled(self): + return self._enabled + def update(self): + if abs(self._progress - self._target) > 0.01: + delta = rl.get_frame_time() * ANIMATION_SPEED + self._progress += delta if self._progress < self._target else -delta + self._progress = max(0.0, min(1.0, self._progress)) -if __name__ == "__main__": - from openpilot.system.ui.lib.application import gui_app + def _render(self, rect: rl.Rectangle): + self.update() - gui_app.init_window("Text toggle example") - toggle = Toggle(100, 100) - for _ in gui_app.render(): - toggle.handle_input() - toggle.render() + if self._enabled: + bg_color = self._blend_color(OFF_COLOR, ON_COLOR, self._progress) + knob_color = KNOB_COLOR + else: + bg_color = self._blend_color(DISABLED_OFF_COLOR, DISABLED_ON_COLOR, self._progress) + knob_color = DISABLED_KNOB_COLOR + # Draw background + bg_rect = rl.Rectangle(self._rect.x + 5, self._rect.y + 10, WIDTH - 10, BG_HEIGHT) + rl.draw_rectangle_rounded(bg_rect, 1.0, 10, bg_color) + + # Draw knob + knob_x = self._rect.x + HEIGHT / 2 + (WIDTH - HEIGHT) * self._progress + knob_y = self._rect.y + HEIGHT / 2 + rl.draw_circle(int(knob_x), int(knob_y), HEIGHT / 2, knob_color) + + def _blend_color(self, c1, c2, t): + return rl.Color(int(c1.r + (c2.r - c1.r) * t), int(c1.g + (c2.g - c1.g) * t), int(c1.b + (c2.b - c1.b) * t), 255) diff --git a/system/ui/lib/widget.py b/system/ui/lib/widget.py new file mode 100644 index 0000000000..3539f5594b --- /dev/null +++ b/system/ui/lib/widget.py @@ -0,0 +1,96 @@ +import abc +import pyray as rl +from enum import IntEnum +from collections.abc import Callable + + +class DialogResult(IntEnum): + CANCEL = 0 + CONFIRM = 1 + NO_ACTION = -1 + + +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_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 + + @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) + self._rect = rect + if changed: + self._update_layout_rects() + + def set_parent_rect(self, parent_rect: rl.Rectangle) -> None: + """Can be used like size hint in QT""" + self._parent_rect = parent_rect + + 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 + if changed: + self._update_layout_rects() + + def render(self, rect: rl.Rectangle = None) -> bool | int | None: + if rect is not None: + self.set_rect(rect) + + self._update_state() + + if not self.is_visible: + return None + + ret = self._render(self._rect) + + # Keep track of whether mouse down started within the widget's rectangle + mouse_pos = rl.get_mouse_position() + if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._touch_valid(): + if rl.check_collision_point_rec(mouse_pos, self._rect): + self._is_pressed = True + + elif not self._touch_valid(): + self._is_pressed = False + + elif rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): + if self._is_pressed and rl.check_collision_point_rec(mouse_pos, self._rect): + self._handle_mouse_release(mouse_pos) + self._is_pressed = False + + return ret + + @abc.abstractmethod + def _render(self, rect: rl.Rectangle) -> bool | int | None: + """Render the widget within the given rectangle.""" + + def _update_state(self): + """Optionally update the widget's non-layout state. This is called before rendering.""" + + def _update_layout_rects(self) -> None: + """Optionally update any layout rects on Widget rect change.""" + + def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool: + """Optionally handle mouse release events.""" + return False diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 96726bd999..dd08b66373 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -13,6 +13,7 @@ from dbus_next.aio import MessageBus from dbus_next import BusType, Variant, Message from dbus_next.errors import DBusError from dbus_next.constants import MessageType + try: from openpilot.common.params import Params except ImportError: @@ -38,6 +39,7 @@ NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 TETHERING_IP_ADDRESS = "192.168.43.1" DEFAULT_TETHERING_PASSWORD = "12345678" + # NetworkManager device states class NMDeviceState(IntEnum): DISCONNECTED = 30 @@ -46,6 +48,7 @@ class NMDeviceState(IntEnum): IP_CONFIG = 70 ACTIVATED = 100 + class SecurityType(IntEnum): OPEN = 0 WPA = 1 @@ -53,6 +56,7 @@ class SecurityType(IntEnum): WPA3 = 3 UNSUPPORTED = 4 + @dataclass class NetworkInfo: ssid: str @@ -69,8 +73,9 @@ class NetworkInfo: class WifiManagerCallbacks: need_auth: Callable[[str], None] | None = None activated: Callable[[], None] | None = None - forgotten: Callable[[], None] | None = None + forgotten: Callable[[str], None] | None = None networks_updated: Callable[[list[NetworkInfo]], None] | None = None + connection_failed: Callable[[str, str], None] | None = None # Added for error feedback class WifiManager: @@ -98,8 +103,8 @@ class WifiManager: self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect() if not await self._find_wifi_device(): raise ValueError("No Wi-Fi device found") - await self._setup_signals(self.device_path) + await self._setup_signals(self.device_path) self.active_ap_path = await self.get_active_access_point() await self.add_tethering_connection(self._tethering_ssid, DEFAULT_TETHERING_PASSWORD) self.saved_connections = await self._get_saved_connections() @@ -122,7 +127,7 @@ class WifiManager: if self.bus: self.bus.disconnect() - async def request_scan(self) -> None: + async def _request_scan(self) -> None: try: interface = self.device_proxy.get_interface(NM_WIRELESS_IFACE) await interface.call_request_scan({}) @@ -146,12 +151,23 @@ class WifiManager: try: nm_iface = await self._get_interface(NM, path, NM_CONNECTION_IFACE) await nm_iface.call_delete() + if self._current_connection_ssid == ssid: self._current_connection_ssid = None if ssid in self.saved_connections: del self.saved_connections[ssid] + for network in self.networks: + if network.ssid == ssid: + network.is_saved = False + network.is_connected = False + break + + # Notify UI of forgotten connection + if self.callbacks.networks_updated: + self.callbacks.networks_updated(copy.deepcopy(self.networks)) + return True except DBusError as e: cloudlog.error(f"Failed to delete connection for SSID: {ssid}. Error: {e}") @@ -212,10 +228,12 @@ class WifiManager: nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) await nm_iface.call_add_and_activate_connection(connection, self.device_path, "/") - await self._update_connection_status() - except DBusError as e: + except Exception as e: self._current_connection_ssid = None cloudlog.error(f"Error connecting to network: {e}") + # Notify UI of failure + if self.callbacks.connection_failed: + self.callbacks.connection_failed(ssid, str(e)) def is_saved(self, ssid: str) -> bool: return ssid in self.saved_connections @@ -393,8 +411,7 @@ class WifiManager: async def _periodic_scan(self): while self.running: try: - await self.request_scan() - await self._get_available_networks() + await self._request_scan() await asyncio.sleep(30) except asyncio.CancelledError: break @@ -423,21 +440,24 @@ class WifiManager: def _on_properties_changed(self, interface: str, changed: dict, invalidated: list): # print("property changed", interface, changed, invalidated) if 'LastScan' in changed: - asyncio.create_task(self._get_available_networks()) + asyncio.create_task(self._refresh_networks()) elif interface == NM_WIRELESS_IFACE and "ActiveAccessPoint" in changed: - self.active_ap_path = changed["ActiveAccessPoint"].value - asyncio.create_task(self._get_available_networks()) + 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(f"State changed: {old_state} -> {new_state}, reason: {reason}") + print("State changed", new_state, old_state, reason) if new_state == NMDeviceState.ACTIVATED: if self.callbacks.activated: self.callbacks.activated() - asyncio.create_task(self._update_connection_status()) + asyncio.create_task(self._refresh_networks()) self._current_connection_ssid = None elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH): for network in self.networks: network.is_connected = False + if new_state == NMDeviceState.NEED_AUTH and reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and self.callbacks.need_auth: if self._current_connection_ssid: self.callbacks.need_auth(self._current_connection_ssid) @@ -453,19 +473,19 @@ class WifiManager: def _on_new_connection(self, path: str) -> None: """Callback for NewConnection signal.""" - print(f"New connection added: {path}") asyncio.create_task(self._add_saved_connection(path)) def _on_connection_removed(self, path: str) -> None: """Callback for ConnectionRemoved signal.""" - print(f"Connection removed: {path}") for ssid, p in list(self.saved_connections.items()): if path == p: del self.saved_connections[ssid] + if self.callbacks.forgotten: - self.callbacks.forgotten() + self.callbacks.forgotten(ssid) + # Update network list to reflect the removed saved connection - asyncio.create_task(self._update_connection_status()) + asyncio.create_task(self._refresh_networks()) break async def _add_saved_connection(self, path: str) -> None: @@ -474,7 +494,7 @@ class WifiManager: settings = await self._get_connection_settings(path) if ssid := self._extract_ssid(settings): self.saved_connections[ssid] = path - await self._update_connection_status() + await self._refresh_networks() except DBusError as e: cloudlog.error(f"Failed to add connection {path}: {e}") @@ -483,10 +503,6 @@ class WifiManager: 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 - async def _update_connection_status(self): - self.active_ap_path = await self.get_active_access_point() - await self._get_available_networks() - async def _add_match_rule(self, rule): """Add a match rule on the bus.""" reply = await self.bus.call( @@ -504,10 +520,11 @@ class WifiManager: assert reply.message_type == MessageType.METHOD_RETURN return reply - async def _get_available_networks(self): + async def _refresh_networks(self): """Get a list of available networks via NetworkManager.""" wifi_iface = self.device_proxy.get_interface(NM_WIRELESS_IFACE) access_points = await wifi_iface.get_access_points() + self.active_ap_path = await self.get_active_access_point() network_dict = {} for ap_path in access_points: try: @@ -531,7 +548,7 @@ class WifiManager: security_type=self._get_security_type(flags, wpa_flags, rsn_flags), path=ap_path, bssid=bssid, - is_connected=self.active_ap_path == ap_path, + is_connected=self.active_ap_path == ap_path and self._current_connection_ssid != ssid, is_saved=ssid in self.saved_connections ) @@ -555,9 +572,7 @@ class WifiManager: async def _get_connection_settings(self, path): """Fetch connection settings for a specific connection path.""" try: - connection_proxy = await self.bus.introspect(NM, path) - connection = self.bus.get_proxy_object(NM, path, connection_proxy) - settings = connection.get_interface(NM_CONNECTION_IFACE) + settings = await self._get_interface(NM, path, NM_CONNECTION_IFACE) return await settings.call_get_settings() except DBusError as e: cloudlog.error(f"Failed to get settings for {path}: {str(e)}") @@ -660,12 +675,6 @@ class WifiManagerWrapper: return self._run_coroutine(self._manager.connect()) - def request_scan(self): - """Request a scan for Wi-Fi networks.""" - if not self._manager: - return - self._run_coroutine(self._manager.request_scan()) - def forget_connection(self, ssid: str): """Forget a saved Wi-Fi connection.""" if not self._manager: diff --git a/system/ui/lib/window.py b/system/ui/lib/window.py deleted file mode 100644 index 989a3b0284..0000000000 --- a/system/ui/lib/window.py +++ /dev/null @@ -1,58 +0,0 @@ -import threading -import time -import os -from typing import Generic, Protocol, TypeVar -from openpilot.common.swaglog import cloudlog -from openpilot.system.ui.lib.application import gui_app - - -class RendererProtocol(Protocol): - def render(self): ... - - -R = TypeVar("R", bound=RendererProtocol) - - -class BaseWindow(Generic[R]): - def __init__(self, title: str): - self._title = title - self._renderer: R | None = None - self._stop_event = threading.Event() - self._thread = threading.Thread(target=self._run) - self._thread.start() - - # wait for the renderer to be initialized - while self._renderer is None and self._thread.is_alive(): - time.sleep(0.01) - - def _create_renderer(self) -> R: - raise NotImplementedError() - - def _run(self): - if os.getenv("CI") is not None: - return - gui_app.init_window(self._title) - self._renderer = self._create_renderer() - try: - for _ in gui_app.render(): - if self._stop_event.is_set(): - break - self._renderer.render() - finally: - gui_app.close() - - def __enter__(self): - return self - - def close(self): - if self._thread.is_alive(): - self._stop_event.set() - self._thread.join(timeout=2.0) - if self._thread.is_alive(): - cloudlog.warning(f"Failed to join {self._title} thread") - - def __del__(self): - self.close() - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() diff --git a/system/ui/lib/wrap_text.py b/system/ui/lib/wrap_text.py new file mode 100644 index 0000000000..35dc5ac401 --- /dev/null +++ b/system/ui/lib/wrap_text.py @@ -0,0 +1,103 @@ +import pyray as rl +from openpilot.system.ui.lib.text_measure import measure_text_cached + + +def _break_long_word(font: rl.Font, word: str, font_size: int, max_width: int) -> list[str]: + if not word: + return [] + + parts = [] + remaining = word + + while remaining: + if measure_text_cached(font, remaining, font_size).x <= max_width: + parts.append(remaining) + break + + # Binary search for the longest substring that fits + left, right = 1, len(remaining) + best_fit = 1 + + while left <= right: + mid = (left + right) // 2 + substring = remaining[:mid] + width = measure_text_cached(font, substring, font_size).x + + if width <= max_width: + best_fit = mid + left = mid + 1 + else: + right = mid - 1 + + # Add the part that fits + parts.append(remaining[:best_fit]) + remaining = remaining[best_fit:] + + return parts + + +def wrap_text(font: rl.Font, text: str, font_size: int, max_width: int) -> list[str]: + if not text or max_width <= 0: + return [] + + # Split text by newlines first to preserve explicit line breaks + paragraphs = text.split('\n') + all_lines: list[str] = [] + + for paragraph in paragraphs: + # Handle empty paragraphs (preserve empty lines) + if not paragraph.strip(): + all_lines.append("") + continue + + # Process each paragraph separately + words = paragraph.split() + if not words: + all_lines.append("") + continue + + lines: list[str] = [] + current_line: list[str] = [] + current_width = 0 + space_width = int(measure_text_cached(font, " ", font_size).x) + + for word in words: + word_width = int(measure_text_cached(font, word, font_size).x) + + # Check if word alone exceeds max width (need to break the word) + if word_width > max_width: + # Finish current line if it has content + if current_line: + lines.append(" ".join(current_line)) + current_line = [] + current_width = 0 + + # Break the long word into parts + lines.extend(_break_long_word(font, word, font_size, max_width)) + continue + + # Calculate width if we add this word + needed_width = current_width + if current_line: # Need space before word + needed_width += space_width + needed_width += word_width + + # Check if word fits on current line + if needed_width <= max_width: + current_line.append(word) + current_width = needed_width + else: + # Start new line with this word + if current_line: + lines.append(" ".join(current_line)) + current_line = [word] + current_width = word_width + + # Add remaining words + if current_line: + lines.append(" ".join(current_line)) + + # Add all lines from this paragraph + all_lines.extend(lines) + + return all_lines diff --git a/system/ui/reset.py b/system/ui/reset.py index 20b689934b..c0dbd1d956 100755 --- a/system/ui/reset.py +++ b/system/ui/reset.py @@ -9,6 +9,7 @@ from openpilot.system.hardware import PC from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.label import gui_label, gui_text_box +from openpilot.system.ui.lib.widget import Widget NVME = "/dev/nvme0n1" USERDATA = "/dev/disk/by-partlabel/userdata" @@ -27,8 +28,9 @@ class ResetState(IntEnum): FAILED = 3 -class Reset: +class Reset(Widget): def __init__(self, mode): + super().__init__() self.mode = mode self.reset_state = ResetState.NONE @@ -54,7 +56,7 @@ class Reset: self.reset_state = ResetState.RESETTING threading.Timer(0.1, self._do_erase).start() - def render(self, rect: rl.Rectangle): + def _render(self, rect: rl.Rectangle): label_rect = rl.Rectangle(rect.x + 140, rect.y, rect.width - 280, 100) gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD) @@ -76,7 +78,7 @@ class Reset: if self.reset_state != ResetState.FAILED: if gui_button(rl.Rectangle(rect.x + button_width + 50, button_top, button_width, button_height), - "Confirm", button_style=ButtonStyle.PRIMARY): + "Confirm", button_style=ButtonStyle.PRIMARY): self.confirm() return True diff --git a/system/ui/setup.py b/system/ui/setup.py index 0ec5d6395e..4d9d269480 100755 --- a/system/ui/setup.py +++ b/system/ui/setup.py @@ -12,6 +12,7 @@ from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.label import gui_label, gui_text_box +from openpilot.system.ui.lib.widget import Widget from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper from openpilot.system.ui.widgets.keyboard import Keyboard @@ -39,8 +40,9 @@ class SetupState(IntEnum): DOWNLOAD_FAILED = 6 -class Setup: +class Setup(Widget): def __init__(self): + super().__init__() self.state = SetupState.GETTING_STARTED self.network_check_thread = None self.network_connected = threading.Event() @@ -67,7 +69,7 @@ class Setup: except (FileNotFoundError, ValueError): self.state = SetupState.LOW_VOLTAGE - def render(self, rect: rl.Rectangle): + def _render(self, rect: rl.Rectangle): if self.state == SetupState.LOW_VOLTAGE: self.render_low_voltage(rect) elif self.state == SetupState.GETTING_STARTED: @@ -116,7 +118,6 @@ class Setup: if ret: self.state = SetupState.NETWORK_SETUP - self.wifi_manager.request_scan() self.start_network_check() def check_network_connectivity(self): @@ -144,10 +145,6 @@ class Setup: self.network_check_thread.join() def render_network_setup(self, rect: rl.Rectangle): - if self.wifi_ui.require_full_screen: - self.wifi_ui.render(rect) - return - 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) @@ -256,18 +253,20 @@ class Setup: self.state = SetupState.GETTING_STARTED def render_custom_url(self): - result = self.keyboard.render("Enter URL", "for Custom Software") + def handle_keyboard_result(result): + # Enter pressed + if result == 1: + url = self.keyboard.text + self.keyboard.clear() + if url: + self.download(url) - # Enter pressed - if result == 1: - url = self.keyboard.text - self.keyboard.clear() - if url: - self.download(url) + # Cancel pressed + elif result == 0: + self.state = SetupState.SOFTWARE_SELECTION - # Cancel pressed - elif result == 0: - self.state = SetupState.SOFTWARE_SELECTION + self.keyboard.set_title("Enter URL", "for Custom Software") + gui_app.set_modal_overlay(self.keyboard, callback=handle_keyboard_result) def download(self, url: str): # autocomplete incomplete URLs diff --git a/system/ui/spinner.py b/system/ui/spinner.py index 2af24c4e51..279ddf034d 100755 --- a/system/ui/spinner.py +++ b/system/ui/spinner.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 import pyray as rl -import threading -import time +import select +import sys from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.window import BaseWindow +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.widget import Widget from openpilot.system.ui.text import wrap_text +from openpilot.system.ui.sunnypilot.lib.application import gui_app_sp + # Constants PROGRESS_BAR_WIDTH = 1000 PROGRESS_BAR_HEIGHT = 20 @@ -22,41 +25,36 @@ def clamp(value, min_value, max_value): return max(min(value, max_value), min_value) -class SpinnerRenderer: +class Spinner(Widget): def __init__(self): - self._comma_texture = gui_app.texture("images/spinner_comma.png", TEXTURE_SIZE, TEXTURE_SIZE) + super().__init__() + self._comma_texture = gui_app_sp.sp_texture("images/spinner_sunnypilot.png", TEXTURE_SIZE, TEXTURE_SIZE) self._spinner_texture = gui_app.texture("images/spinner_track.png", TEXTURE_SIZE, TEXTURE_SIZE, alpha_premultiply=True) self._rotation = 0.0 self._progress: int | None = None self._wrapped_lines: list[str] = [] - self._lock = threading.Lock() def set_text(self, text: str) -> None: - with self._lock: - if text.isdigit(): - self._progress = clamp(int(text), 0, 100) - self._wrapped_lines = [] - else: - self._progress = None - self._wrapped_lines = wrap_text(text, FONT_SIZE, gui_app.width - MARGIN_H) + if text.isdigit(): + self._progress = clamp(int(text), 0, 100) + self._wrapped_lines = [] + else: + self._progress = None + self._wrapped_lines = wrap_text(text, FONT_SIZE, gui_app.width - MARGIN_H) - def render(self): - with self._lock: - progress = self._progress - wrapped_lines = self._wrapped_lines - - if wrapped_lines: + def _render(self, rect: rl.Rectangle): + if self._wrapped_lines: # Calculate total height required for spinner and text spacing = 50 - total_height = TEXTURE_SIZE + spacing + len(wrapped_lines) * LINE_HEIGHT - center_y = (gui_app.height - total_height) / 2.0 + TEXTURE_SIZE / 2.0 + total_height = TEXTURE_SIZE + spacing + len(self._wrapped_lines) * LINE_HEIGHT + center_y = (rect.height - total_height) / 2.0 + TEXTURE_SIZE / 2.0 else: # Center spinner vertically spacing = 150 - center_y = gui_app.height / 2.0 + center_y = rect.height / 2.0 y_pos = center_y + TEXTURE_SIZE / 2.0 + spacing - center = rl.Vector2(gui_app.width / 2.0, center_y) + center = rl.Vector2(rect.width / 2.0, center_y) spinner_origin = rl.Vector2(TEXTURE_SIZE / 2.0, TEXTURE_SIZE / 2.0) comma_position = rl.Vector2(center.x - TEXTURE_SIZE / 2.0, center.y - TEXTURE_SIZE / 2.0) @@ -70,38 +68,42 @@ class SpinnerRenderer: rl.draw_texture_v(self._comma_texture, comma_position, rl.WHITE) # Display the progress bar or text based on user input - if progress is not None: + if self._progress is not None: bar = rl.Rectangle(center.x - PROGRESS_BAR_WIDTH / 2.0, y_pos, PROGRESS_BAR_WIDTH, PROGRESS_BAR_HEIGHT) rl.draw_rectangle_rounded(bar, 1, 10, DARKGRAY) - bar.width *= progress / 100.0 + bar.width *= self._progress / 100.0 rl.draw_rectangle_rounded(bar, 1, 10, rl.WHITE) - elif wrapped_lines: - for i, line in enumerate(wrapped_lines): - text_size = rl.measure_text_ex(gui_app.font(), line, FONT_SIZE, 0.0) + elif self._wrapped_lines: + for i, line in enumerate(self._wrapped_lines): + text_size = measure_text_cached(gui_app.font(), line, FONT_SIZE) rl.draw_text_ex(gui_app.font(), line, rl.Vector2(center.x - text_size.x / 2, y_pos + i * LINE_HEIGHT), FONT_SIZE, 0.0, rl.WHITE) -class Spinner(BaseWindow[SpinnerRenderer]): - def __init__(self): - super().__init__("Spinner") - - def _create_renderer(self): - return SpinnerRenderer() - - def update(self, spinner_text: str): - if self._renderer is not None: - self._renderer.set_text(spinner_text) - - def update_progress(self, cur: float, total: float): - self.update(str(round(100 * cur / total))) +def _read_stdin(): + """Non-blocking read of available lines from stdin.""" + lines = [] + while True: + rlist, _, _ = select.select([sys.stdin], [], [], 0.0) + if not rlist: + break + line = sys.stdin.readline().strip() + if line == "": + break + lines.append(line) + return lines def main(): - with Spinner() as s: - s.update("Spinner text") - time.sleep(5) + gui_app.init_window("Spinner") + spinner = Spinner() + for _ in gui_app.render(): + text_list = _read_stdin() + if text_list: + spinner.set_text(text_list[-1]) + + spinner.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) if __name__ == "__main__": diff --git a/system/ui/sunnypilot/lib/application.py b/system/ui/sunnypilot/lib/application.py new file mode 100644 index 0000000000..10227ce9fd --- /dev/null +++ b/system/ui/sunnypilot/lib/application.py @@ -0,0 +1,21 @@ +from openpilot.system.ui.lib.application import GuiApplication +from importlib.resources import as_file, files + +ASSETS_DIR_SP = files("openpilot.sunnypilot.selfdrive").joinpath("assets") + +class GuiApplicationSP(GuiApplication): + + def __init__(self, width: int, height: int): + super().__init__(width, height) + + def sp_texture(self, asset_path: str, width: int, height: int, alpha_premultiply=False, keep_aspect_ratio=True): + cache_key = f"{asset_path}_{width}_{height}_{alpha_premultiply}{keep_aspect_ratio}" + if cache_key in self._textures: + return self._textures[cache_key] + + with as_file(ASSETS_DIR_SP.joinpath(asset_path)) as fspath: + texture_obj = self._load_texture_from_image(fspath.as_posix(), width, height, alpha_premultiply, keep_aspect_ratio) + self._textures[cache_key] = texture_obj + return texture_obj + +gui_app_sp = GuiApplicationSP(2160, 1080) diff --git a/system/ui/text.py b/system/ui/text.py index 82e64d836f..01a580a12b 100755 --- a/system/ui/text.py +++ b/system/ui/text.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 import re -import time +import sys import pyray as rl from openpilot.system.hardware import HARDWARE, PC +from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.window import BaseWindow +from openpilot.system.ui.lib.widget import Widget MARGIN = 50 SPACING = 40 @@ -17,6 +18,7 @@ BUTTON_SIZE = rl.Vector2(310, 160) DEMO_TEXT = """This is a sample text that will be wrapped and scrolled if necessary. The text is long enough to demonstrate scrolling and word wrapping.""" * 30 + def wrap_text(text, font_size, max_width): lines = [] font = gui_app.font() @@ -33,7 +35,7 @@ def wrap_text(text, font_size, max_width): while len(words): word = words.pop(0) test_line = current_line + word + (words.pop(0) if words else "") - if rl.measure_text_ex(font, test_line, font_size, 0).x <= max_width: + if measure_text_cached(font, test_line, font_size).x <= max_width: current_line = test_line else: lines.append(current_line) @@ -45,15 +47,16 @@ def wrap_text(text, font_size, max_width): return lines -class TextWindowRenderer: +class TextWindow(Widget): def __init__(self, text: str): + super().__init__() self._textarea_rect = rl.Rectangle(MARGIN, MARGIN, gui_app.width - MARGIN * 2, gui_app.height - MARGIN * 2) self._wrapped_lines = wrap_text(text, FONT_SIZE, self._textarea_rect.width - 20) self._content_rect = rl.Rectangle(0, 0, self._textarea_rect.width - 20, len(self._wrapped_lines) * LINE_HEIGHT) self._scroll_panel = GuiScrollPanel(show_vertical_scroll_bar=True) self._scroll_panel._offset.y = -max(self._content_rect.height - self._textarea_rect.height, 0) - def render(self): + def _render(self, rect: rl.Rectangle): scroll = self._scroll_panel.handle_scroll(self._textarea_rect, self._content_rect) rl.begin_scissor_mode(int(self._textarea_rect.x), int(self._textarea_rect.y), int(self._textarea_rect.width), int(self._textarea_rect.height)) for i, line in enumerate(self._wrapped_lines): @@ -63,7 +66,7 @@ class TextWindowRenderer: rl.draw_text_ex(gui_app.font(), line, position, FONT_SIZE, 0, rl.WHITE) rl.end_scissor_mode() - button_bounds = rl.Rectangle(gui_app.width - MARGIN - BUTTON_SIZE.x - SPACING, gui_app.height - MARGIN - BUTTON_SIZE.y, BUTTON_SIZE.x, BUTTON_SIZE.y) + button_bounds = rl.Rectangle(rect.width - MARGIN - BUTTON_SIZE.x - SPACING, rect.height - MARGIN - BUTTON_SIZE.y, BUTTON_SIZE.x, BUTTON_SIZE.y) ret = gui_button(button_bounds, "Exit" if PC else "Reboot", button_style=ButtonStyle.TRANSPARENT) if ret: if PC: @@ -73,19 +76,9 @@ class TextWindowRenderer: return ret -class TextWindow(BaseWindow[TextWindowRenderer]): - def __init__(self, text: str): - self._text = text - super().__init__("Text") - - def _create_renderer(self): - return TextWindowRenderer(self._text) - - def wait_for_exit(self): - while self._thread.is_alive(): - time.sleep(0.01) - - if __name__ == "__main__": - with TextWindow(DEMO_TEXT): - time.sleep(30) + text = sys.argv[1] if len(sys.argv) > 1 else DEMO_TEXT + gui_app.init_window("Text Viewer") + text_window = TextWindow(text) + for _ in gui_app.render(): + text_window.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) diff --git a/system/ui/updater.py b/system/ui/updater.py index 3ee02ce97c..3fd60cfb1d 100755 --- a/system/ui/updater.py +++ b/system/ui/updater.py @@ -10,9 +10,9 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.label import gui_text_box, gui_label from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper +from openpilot.system.ui.lib.widget import Widget from openpilot.system.ui.widgets.network import WifiManagerUI - # Constants MARGIN = 50 BUTTON_HEIGHT = 160 @@ -31,8 +31,9 @@ class Screen(IntEnum): PROGRESS = 2 -class Updater: +class Updater(Widget): def __init__(self, updater_path, manifest_path): + super().__init__() self.updater = updater_path self.manifest = manifest_path self.current_screen = Screen.PROMPT @@ -60,7 +61,7 @@ class Updater: # TODO: just import it and run in a thread without a subprocess cmd = [self.updater, "--swap", self.manifest] self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True, bufsize=1, universal_newlines=True) + text=True, bufsize=1, universal_newlines=True) for line in self.process.stdout: parts = line.strip().split(":") @@ -78,21 +79,21 @@ class Updater: self.progress_text = "Update failed" self.show_reboot_button = True - def render_prompt_screen(self): + def render_prompt_screen(self, rect: rl.Rectangle): # Title - title_rect = rl.Rectangle(MARGIN + 50, 250, gui_app.width - MARGIN * 2 - 100, TITLE_FONT_SIZE) + title_rect = rl.Rectangle(MARGIN + 50, 250, rect.width - MARGIN * 2 - 100, TITLE_FONT_SIZE) gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD) # Description desc_text = ("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " + - "The download size is approximately 1GB.") + "The download size is approximately 1GB.") - desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE + 75, gui_app.width - MARGIN * 2 - 100, BODY_FONT_SIZE * 3) + desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE + 75, rect.width - MARGIN * 2 - 100, BODY_FONT_SIZE * 3) gui_text_box(desc_rect, desc_text, BODY_FONT_SIZE) # Buttons at the bottom - button_y = gui_app.height - MARGIN - BUTTON_HEIGHT - button_width = (gui_app.width - MARGIN * 3) // 2 + button_y = rect.height - MARGIN - BUTTON_HEIGHT + button_width = (rect.width - MARGIN * 3) // 2 # WiFi button wifi_button_rect = rl.Rectangle(MARGIN, button_y, button_width, BUTTON_HEIGHT) @@ -106,24 +107,22 @@ class Updater: self.install_update() return # Return to avoid further processing after action - def render_wifi_screen(self): + def render_wifi_screen(self, rect: rl.Rectangle): # Draw the Wi-Fi manager UI - wifi_rect = rl.Rectangle(MARGIN + 50, MARGIN, gui_app.width - MARGIN * 2 - 100, gui_app.height - MARGIN * 2 - BUTTON_HEIGHT - 20) + wifi_rect = rl.Rectangle(MARGIN + 50, MARGIN, rect.width - MARGIN * 2 - 100, rect.height - MARGIN * 2 - BUTTON_HEIGHT - 20) self.wifi_manager_ui.render(wifi_rect) - if self.wifi_manager_ui.require_full_screen: - return - back_button_rect = rl.Rectangle(MARGIN, gui_app.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT) + back_button_rect = rl.Rectangle(MARGIN, rect.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT) if gui_button(back_button_rect, "Back"): self.current_screen = Screen.PROMPT return # Return to avoid processing other interactions after screen change - def render_progress_screen(self): - title_rect = rl.Rectangle(MARGIN + 100, 330, gui_app.width - MARGIN * 2 - 200, 100) + def render_progress_screen(self, rect: rl.Rectangle): + title_rect = rl.Rectangle(MARGIN + 100, 330, rect.width - MARGIN * 2 - 200, 100) gui_label(title_rect, self.progress_text, 90, font_weight=FontWeight.SEMI_BOLD) # Progress bar - bar_rect = rl.Rectangle(MARGIN + 100, 330 + 100 + 100, gui_app.width - MARGIN * 2 - 200, PROGRESS_BAR_HEIGHT) + bar_rect = rl.Rectangle(MARGIN + 100, 330 + 100 + 100, rect.width - MARGIN * 2 - 200, PROGRESS_BAR_HEIGHT) rl.draw_rectangle_rounded(bar_rect, 0.5, 10, PROGRESS_BG_COLOR) # Calculate the width of the progress chunk @@ -134,19 +133,19 @@ class Updater: # Show reboot button if needed if self.show_reboot_button: - reboot_rect = rl.Rectangle(MARGIN + 100, gui_app.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT) + reboot_rect = rl.Rectangle(MARGIN + 100, rect.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT) if gui_button(reboot_rect, "Reboot"): # Return True to signal main loop to exit before rebooting HARDWARE.reboot() return - def render(self): + def _render(self, rect: rl.Rectangle): if self.current_screen == Screen.PROMPT: - self.render_prompt_screen() + self.render_prompt_screen(rect) elif self.current_screen == Screen.WIFI: - self.render_wifi_screen() + self.render_wifi_screen(rect) elif self.current_screen == Screen.PROGRESS: - self.render_progress_screen() + self.render_progress_screen(rect) def main(): @@ -161,7 +160,7 @@ def main(): gui_app.init_window("System Update") updater = Updater(updater_path, manifest_path) for _ in gui_app.render(): - updater.render() + updater.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) finally: # Make sure we clean up even if there's an error gui_app.close() diff --git a/system/ui/widgets/confirm_dialog.py b/system/ui/widgets/confirm_dialog.py index f42220053b..00361905d6 100644 --- a/system/ui/widgets/confirm_dialog.py +++ b/system/ui/widgets/confirm_dialog.py @@ -1,9 +1,9 @@ import pyray as rl -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.label import gui_text_box +from openpilot.system.ui.lib.widget import DialogResult -# Constants for dialog dimensions and styling DIALOG_WIDTH = 1520 DIALOG_HEIGHT = 600 BUTTON_HEIGHT = 160 @@ -12,7 +12,7 @@ TEXT_AREA_HEIGHT_REDUCTION = 200 BACKGROUND_COLOR = rl.Color(27, 27, 27, 255) -def confirm_dialog(message: str, confirm_text: str, cancel_text: str = "Cancel") -> int: +def confirm_dialog(message: str, confirm_text: str, cancel_text: str = "Cancel") -> DialogResult: 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) @@ -30,28 +30,39 @@ def confirm_dialog(message: str, confirm_text: str, cancel_text: str = "Cancel") rl.draw_rectangle_rec(dialog_rect, BACKGROUND_COLOR) # Draw the message in the dialog, centered - text_rect = rl.Rectangle(dialog_rect.x, dialog_rect.y, dialog_rect.width, dialog_rect.height - TEXT_AREA_HEIGHT_REDUCTION) + text_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y, dialog_rect.width - 2 * MARGIN, dialog_rect.height - TEXT_AREA_HEIGHT_REDUCTION) gui_text_box( text_rect, message, - font_size=88, + font_size=70, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, + font_weight=FontWeight.BOLD, ) # Initialize result; -1 means no action taken yet - result = -1 + result = DialogResult.NO_ACTION # Check for keyboard input for accessibility if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER): - result = 1 # Confirm + result = DialogResult.CONFIRM elif rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE): - result = 0 # Cancel + result = DialogResult.CANCEL # Check for button clicks - if gui_button(yes_button, confirm_text, button_style=ButtonStyle.PRIMARY): - result = 1 # Confirm - if gui_button(no_button, cancel_text): - result = 0 # Cancel + if cancel_text: + if gui_button(yes_button, confirm_text, button_style=ButtonStyle.PRIMARY): + result = DialogResult.CONFIRM + if gui_button(no_button, cancel_text): + result = DialogResult.CANCEL + else: + centered_button_x = dialog_rect.x + (dialog_rect.width - button_width) / 2 + centered_yes_button = rl.Rectangle(centered_button_x, button_y, button_width, BUTTON_HEIGHT) + if gui_button(centered_yes_button, confirm_text, button_style=ButtonStyle.PRIMARY): + result = DialogResult.CONFIRM return result + + +def alert_dialog(message: str, button_text: str = "OK") -> DialogResult: + return confirm_dialog(message, button_text, cancel_text="") diff --git a/system/ui/widgets/html_render.py b/system/ui/widgets/html_render.py new file mode 100644 index 0000000000..07741af456 --- /dev/null +++ b/system/ui/widgets/html_render.py @@ -0,0 +1,194 @@ +import re +import pyray as rl +from dataclasses import dataclass +from enum import Enum +from typing import Any +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.lib.widget import Widget, DialogResult +from openpilot.system.ui.lib.button import gui_button, ButtonStyle + + +class ElementType(Enum): + H1 = "h1" + H2 = "h2" + H3 = "h3" + H4 = "h4" + H5 = "h5" + H6 = "h6" + P = "p" + BR = "br" + + +@dataclass +class HtmlElement: + type: ElementType + content: str + font_size: int + font_weight: FontWeight + color: rl.Color + margin_top: int + margin_bottom: int + line_height: float = 1.2 + + +class HtmlRenderer(Widget): + def __init__(self, file_path: str): + self.elements: list[HtmlElement] = [] + self._normal_font = gui_app.font(FontWeight.NORMAL) + self._bold_font = gui_app.font(FontWeight.BOLD) + self._scroll_panel = GuiScrollPanel() + + self.styles: dict[ElementType, dict[str, Any]] = { + ElementType.H1: {"size": 68, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 20, "margin_bottom": 16}, + ElementType.H2: {"size": 60, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 24, "margin_bottom": 12}, + ElementType.H3: {"size": 52, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 20, "margin_bottom": 10}, + ElementType.H4: {"size": 48, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 16, "margin_bottom": 8}, + ElementType.H5: {"size": 44, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 12, "margin_bottom": 6}, + ElementType.H6: {"size": 40, "weight": FontWeight.BOLD, "color": rl.BLACK, "margin_top": 10, "margin_bottom": 4}, + ElementType.P: {"size": 38, "weight": FontWeight.NORMAL, "color": rl.Color(40, 40, 40, 255), "margin_top": 8, "margin_bottom": 12}, + ElementType.BR: {"size": 0, "weight": FontWeight.NORMAL, "color": rl.BLACK, "margin_top": 0, "margin_bottom": 12}, + } + + self.parse_html_file(file_path) + + def parse_html_file(self, file_path: str) -> None: + with open(file_path, encoding='utf-8') as file: + content = file.read() + self.parse_html_content(content) + + def parse_html_content(self, html_content: str) -> None: + self.elements.clear() + + # Remove HTML comments + html_content = re.sub(r'', '', html_content, flags=re.DOTALL) + + # Remove DOCTYPE, html, head, body tags but keep their content + html_content = re.sub(r']*>', '', html_content) + html_content = re.sub(r']*>', '', html_content) + + # Find all HTML elements + pattern = r'<(h[1-6]|p)(?:[^>]*)>(.*?)|' + matches = re.finditer(pattern, html_content, re.DOTALL | re.IGNORECASE) + + for match in matches: + if match.group(0).lower().startswith(' tags + self._add_element(ElementType.BR, "") + else: + tag = match.group(1).lower() + content = match.group(2).strip() + + # Clean up content - remove extra whitespace + content = re.sub(r'\s+', ' ', content) + content = content.strip() + + if content: # Only add non-empty elements + element_type = ElementType(tag) + self._add_element(element_type, content) + + def _add_element(self, element_type: ElementType, content: str) -> None: + style = self.styles[element_type] + + element = HtmlElement( + type=element_type, + content=content, + font_size=style["size"], + font_weight=style["weight"], + color=style["color"], + margin_top=style["margin_top"], + margin_bottom=style["margin_bottom"], + ) + + self.elements.append(element) + + def _render(self, rect: rl.Rectangle): + margin = 50 + content_rect = rl.Rectangle(rect.x + margin, rect.y + margin, rect.width - (margin * 2), rect.height - (margin * 2)) + + button_height = 160 + button_spacing = 20 + scrollable_height = content_rect.height - button_height - button_spacing + + scrollable_rect = rl.Rectangle(content_rect.x, content_rect.y, content_rect.width, scrollable_height) + + total_height = self.get_total_height(int(scrollable_rect.width)) + scroll_content_rect = rl.Rectangle(scrollable_rect.x, scrollable_rect.y, scrollable_rect.width, total_height) + scroll_offset = self._scroll_panel.handle_scroll(scrollable_rect, scroll_content_rect) + + rl.begin_scissor_mode(int(scrollable_rect.x), int(scrollable_rect.y), int(scrollable_rect.width), int(scrollable_rect.height)) + self._render_content(scrollable_rect, scroll_offset.y) + rl.end_scissor_mode() + + button_width = (rect.width - 3 * 50) // 3 + button_x = content_rect.x + (content_rect.width - button_width) / 2 + button_y = content_rect.y + content_rect.height - button_height + button_rect = rl.Rectangle(button_x, button_y, button_width, button_height) + if gui_button(button_rect, "OK", button_style=ButtonStyle.PRIMARY) == 1: + return DialogResult.CONFIRM + + return DialogResult.NO_ACTION + + def _render_content(self, rect: rl.Rectangle, scroll_offset: float = 0) -> float: + current_y = rect.y + scroll_offset + padding = 20 + content_width = rect.width - (padding * 2) + + for element in self.elements: + if element.type == ElementType.BR: + current_y += element.margin_bottom + continue + + current_y += element.margin_top + if current_y > rect.y + rect.height: + break + + if element.content: + font = self._get_font(element.font_weight) + wrapped_lines = wrap_text(font, element.content, element.font_size, int(content_width)) + + for line in wrapped_lines: + if current_y < rect.y - element.font_size: + current_y += element.font_size * element.line_height + continue + + if current_y > rect.y + rect.height: + break + + rl.draw_text_ex(font, line, rl.Vector2(rect.x + padding, current_y), element.font_size, 0, rl.WHITE) + + current_y += element.font_size * element.line_height + + # Apply bottom margin + current_y += element.margin_bottom + + return current_y - rect.y - scroll_offset # Return total content height + + def get_total_height(self, content_width: int) -> float: + total_height = 0.0 + padding = 20 + usable_width = content_width - (padding * 2) + + for element in self.elements: + if element.type == ElementType.BR: + total_height += element.margin_bottom + continue + + total_height += element.margin_top + + if element.content: + font = self._get_font(element.font_weight) + wrapped_lines = wrap_text(font, element.content, element.font_size, int(usable_width)) + + for _ in wrapped_lines: + total_height += element.font_size * element.line_height + + total_height += element.margin_bottom + + return total_height + + def _get_font(self, weight: FontWeight): + if weight == FontWeight.BOLD: + return self._bold_font + return self._normal_font diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 0e22f1b527..338706e73d 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -5,6 +5,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.button import ButtonStyle, gui_button from openpilot.system.ui.lib.inputbox import InputBox from openpilot.system.ui.lib.label import gui_label +from openpilot.system.ui.lib.widget import Widget KEY_FONT_SIZE = 96 DOUBLE_CLICK_THRESHOLD = 0.5 # seconds @@ -52,11 +53,14 @@ KEYBOARD_LAYOUTS = { } -class Keyboard: +class Keyboard(Widget): def __init__(self, max_text_size: int = 255, min_text_size: int = 0, password_mode: bool = False, show_password_toggle: bool = False): + super().__init__() 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._max_text_size = max_text_size self._min_text_size = min_text_size @@ -67,7 +71,7 @@ class Keyboard: # Backspace key repeat tracking self._backspace_pressed: bool = False self._backspace_press_time: float = 0.0 - self._backspace_last_repeat:float = 0.0 + self._backspace_last_repeat: float = 0.0 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) @@ -89,10 +93,14 @@ class Keyboard: self._input_box.clear() self._backspace_pressed = False - def render(self, title: str, sub_title: str): - rect = rl.Rectangle(CONTENT_MARGIN, CONTENT_MARGIN, gui_app.width - 2 * CONTENT_MARGIN, gui_app.height - 2 * CONTENT_MARGIN) - gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), title, 90, font_weight=FontWeight.BOLD) - gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), sub_title, 55, font_weight=FontWeight.NORMAL) + def set_title(self, title: str, sub_title: str = ""): + self._title = title + self._sub_title = sub_title + + 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 @@ -223,7 +231,8 @@ if __name__ == "__main__": gui_app.init_window("Keyboard") keyboard = Keyboard(min_text_size=8, show_password_toggle=True) for _ in gui_app.render(): - result = keyboard.render("Keyboard", "Type here") + keyboard.set_title("Keyboard Input", "Type your text below") + result = keyboard.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) if result == 1: print(f"You typed: {keyboard.text}") gui_app.request_close() diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 5877b3ff7a..0613bb1f12 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from threading import Lock from typing import Literal import pyray as rl @@ -9,12 +10,13 @@ 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.keyboard import Keyboard from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog +from openpilot.system.ui.lib.widget import Widget NM_DEVICE_STATE_NEED_AUTH = 60 MIN_PASSWORD_LENGTH = 8 MAX_PASSWORD_LENGTH = 64 ITEM_HEIGHT = 160 -ICON_SIZE = 49 +ICON_SIZE = 50 STRENGTH_ICONS = [ "icons/wifi_strength_low.png", @@ -23,83 +25,99 @@ STRENGTH_ICONS = [ "icons/wifi_strength_full.png", ] + @dataclass class StateIdle: action: Literal["idle"] = "idle" + @dataclass class StateConnecting: network: NetworkInfo action: Literal["connecting"] = "connecting" + @dataclass class StateNeedsAuth: network: NetworkInfo action: Literal["needs_auth"] = "needs_auth" + @dataclass class StateShowForgetConfirm: network: NetworkInfo action: Literal["show_forget_confirm"] = "show_forget_confirm" + @dataclass class StateForgetting: network: NetworkInfo action: Literal["forgetting"] = "forgetting" + UIState = StateIdle | StateConnecting | StateNeedsAuth | StateShowForgetConfirm | StateForgetting -class WifiManagerUI: +class WifiManagerUI(Widget): def __init__(self, wifi_manager: WifiManagerWrapper): + super().__init__() self.state: UIState = StateIdle() - self.btn_width = 200 + self.btn_width: int = 200 self.scroll_panel = GuiScrollPanel() self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True) self._networks: list[NetworkInfo] = [] - + self._lock = Lock() self.wifi_manager = wifi_manager - self.wifi_manager.set_callbacks(WifiManagerCallbacks(self._on_need_auth, self._on_activated, self._on_forgotten, self._on_network_updated)) + + self.wifi_manager.set_callbacks( + WifiManagerCallbacks( + need_auth=self._on_need_auth, + activated=self._on_activated, + forgotten=self._on_forgotten, + networks_updated=self._on_network_updated, + connection_failed=self._on_connection_failed + ) + ) self.wifi_manager.start() self.wifi_manager.connect() - def render(self, rect: rl.Rectangle): - if not self._networks: - gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - return + def _render(self, rect: rl.Rectangle): + with self._lock: + if not self._networks: + gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + return - match self.state: - case StateNeedsAuth(network): - result = self.keyboard.render("Enter password", f"for {network.ssid}") - if result == 1: - password = self.keyboard.text - self.keyboard.clear() + match self.state: + case StateNeedsAuth(network): + self.keyboard.set_title("Enter password", f"for {network.ssid}") + 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)) + case _: + self._draw_network_list(rect) - if len(password) >= MIN_PASSWORD_LENGTH: - self.connect_to_network(network, password) - elif result == 0: - self.state = StateIdle() + def _on_password_entered(self, network: NetworkInfo, result: int): + if result == 1: + password = self.keyboard.text + self.keyboard.clear() - case StateShowForgetConfirm(network): - result = confirm_dialog(f'Forget Wi-Fi Network "{network.ssid}"?', "Forget") - if result == 1: - self.forget_network(network) - elif result == 0: - self.state = StateIdle() + if len(password) >= MIN_PASSWORD_LENGTH: + self.connect_to_network(network, password) + elif result == 0: + self.state = StateIdle() - case _: - self._draw_network_list(rect) - - @property - def require_full_screen(self) -> bool: - """Check if the WiFi UI requires exclusive full-screen rendering.""" - return isinstance(self.state, (StateNeedsAuth, StateShowForgetConfirm)) + def on_forgot_confirm_finished(self, network, result: int): + if result == 1: + self.forget_network(network) + elif result == 0: + self.state = StateIdle() def _draw_network_list(self, rect: rl.Rectangle): content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self._networks) * ITEM_HEIGHT) offset = self.scroll_panel.handle_scroll(rect, content_rect) - clicked = self.scroll_panel.is_click_valid() + clicked = self.scroll_panel.is_touch_valid() and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) for i, network in enumerate(self._networks): @@ -138,7 +156,8 @@ class WifiManagerUI: else: # If the network is saved, show the "Forget" button if network.is_saved: - forget_btn_rect = rl.Rectangle(security_icon_rect.x - self.btn_width - spacing, + forget_btn_rect = rl.Rectangle( + security_icon_rect.x - self.btn_width - spacing, rect.y + (ITEM_HEIGHT - 80) / 2, self.btn_width, 80, @@ -190,25 +209,29 @@ class WifiManagerUI: self.wifi_manager.forget_connection(network.ssid) def _on_network_updated(self, networks: list[NetworkInfo]): - self._networks = networks + with self._lock: + self._networks = networks def _on_need_auth(self, ssid): - match self.state: - case StateConnecting(ssid): - self.state = StateNeedsAuth(ssid) - case _: - # Find network by SSID - network = next((n for n in self.wifi_manager.networks if n.ssid == ssid), None) - if network: - self.state = StateNeedsAuth(network) + with self._lock: + network = next((n for n in self._networks if n.ssid == ssid), None) + if network: + self.state = StateNeedsAuth(network) def _on_activated(self): - if isinstance(self.state, StateConnecting): - self.state = StateIdle() + with self._lock: + if isinstance(self.state, StateConnecting): + self.state = StateIdle() - def _on_forgotten(self): - if isinstance(self.state, StateForgetting): - self.state = StateIdle() + def _on_forgotten(self, ssid): + with self._lock: + if isinstance(self.state, StateForgetting): + self.state = StateIdle() + + def _on_connection_failed(self, ssid: str, error: str): + with self._lock: + if isinstance(self.state, StateConnecting): + self.state = StateIdle() def main(): diff --git a/system/ui/widgets/option_dialog.py b/system/ui/widgets/option_dialog.py index c25c5c6e6c..a2958ee1f4 100644 --- a/system/ui/widgets/option_dialog.py +++ b/system/ui/widgets/option_dialog.py @@ -1,81 +1,71 @@ import pyray as rl - +from openpilot.system.ui.lib.application import FontWeight from openpilot.system.ui.lib.button import gui_button, ButtonStyle, TextAlignment from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.lib.widget import Widget + +# Constants +MARGIN = 50 +TITLE_FONT_SIZE = 70 +ITEM_HEIGHT = 135 +BUTTON_SPACING = 50 +BUTTON_HEIGHT = 160 +ITEM_SPACING = 50 +LIST_ITEM_SPACING = 25 -class MultiOptionDialog: +class MultiOptionDialog(Widget): def __init__(self, title, options, current=""): - self._title = title - self._options = options - self._current = current if current in options else "" - self._selection = self._current - self._option_height = 80 - self._padding = 20 - self.scroll_panel = GuiScrollPanel() + super().__init__() + self.title = title + self.options = options + self.current = current + self.selection = current + self.scroll = GuiScrollPanel() - @property - def selection(self): - return self._selection + def _render(self, rect): + dialog_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - 2 * MARGIN, rect.height - 2 * MARGIN) + rl.draw_rectangle_rounded(dialog_rect, 0.02, 20, rl.Color(30, 30, 30, 255)) - def render(self, rect): - title_rect = rl.Rectangle(rect.x + self._padding, rect.y + self._padding, rect.width - 2 * self._padding, 70) - gui_label(title_rect, self._title, 70) + content_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y + MARGIN, + dialog_rect.width - 2 * MARGIN, dialog_rect.height - 2 * MARGIN) - options_y_start = rect.y + 120 - options_height = len(self._options) * (self._option_height + 10) - options_rect = rl.Rectangle(rect.x + self._padding, options_y_start, rect.width - 2 * self._padding, options_height) + gui_label(rl.Rectangle(content_rect.x, content_rect.y, content_rect.width, TITLE_FONT_SIZE), self.title, 70, font_weight=FontWeight.BOLD) - view_rect = rl.Rectangle( - rect.x + self._padding, options_y_start, rect.width - 2 * self._padding, rect.height - 200 - 2 * self._padding - ) + # Options area + options_y = content_rect.y + TITLE_FONT_SIZE + ITEM_SPACING + options_h = content_rect.height - TITLE_FONT_SIZE - BUTTON_HEIGHT - 2 * ITEM_SPACING + view_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, options_h) + content_h = len(self.options) * (ITEM_HEIGHT + 10) + list_content_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, content_h) - offset = self.scroll_panel.handle_scroll(view_rect, options_rect) - is_click_valid = self.scroll_panel.is_click_valid() + # Scroll and render options + offset = self.scroll.handle_scroll(view_rect, list_content_rect) + valid_click = self.scroll.is_touch_valid() and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) - rl.begin_scissor_mode(int(view_rect.x), int(view_rect.y), int(view_rect.width), int(view_rect.height)) + rl.begin_scissor_mode(int(view_rect.x), int(options_y), int(view_rect.width), int(options_h)) + for i, option in enumerate(self.options): + item_y = options_y + i * (ITEM_HEIGHT + LIST_ITEM_SPACING) + offset.y + item_rect = rl.Rectangle(view_rect.x, item_y, view_rect.width, ITEM_HEIGHT) - for i, option in enumerate(self._options): - y_pos = view_rect.y + i * (self._option_height + 10) + offset.y - item_rect = rl.Rectangle(view_rect.x, y_pos, view_rect.width, self._option_height) - - if not rl.check_collision_recs(item_rect, view_rect): - continue - - is_selected = option == self._selection - button_style = ButtonStyle.PRIMARY if is_selected else ButtonStyle.NORMAL - - if gui_button(item_rect, option, button_style=button_style, text_alignment=TextAlignment.LEFT) and is_click_valid: - self._selection = option + if rl.check_collision_recs(item_rect, view_rect): + selected = option == self.selection + style = ButtonStyle.PRIMARY if selected else ButtonStyle.NORMAL + if gui_button(item_rect, option, button_style=style, text_alignment=TextAlignment.LEFT) and valid_click: + self.selection = option rl.end_scissor_mode() - button_y = rect.y + rect.height - 80 - self._padding - button_width = (rect.width - 3 * self._padding) / 2 + # Buttons + button_y = content_rect.y + content_rect.height - BUTTON_HEIGHT + button_w = (content_rect.width - BUTTON_SPACING) / 2 - cancel_rect = rl.Rectangle(rect.x + self._padding, button_y, button_width, 80) - if gui_button(cancel_rect, "Cancel"): - return 0 # Canceled + if gui_button(rl.Rectangle(content_rect.x, button_y, button_w, BUTTON_HEIGHT), "Cancel"): + return 0 - select_rect = rl.Rectangle(rect.x + 2 * self._padding + button_width, button_y, button_width, 80) - has_new_selection = self._selection != "" and self._selection != self._current + if gui_button(rl.Rectangle(content_rect.x + button_w + BUTTON_SPACING, button_y, button_w, BUTTON_HEIGHT), + "Select", is_enabled=self.selection != self.current, button_style=ButtonStyle.PRIMARY): + return 1 - if gui_button(select_rect, "Select", is_enabled=has_new_selection, button_style=ButtonStyle.PRIMARY): - return 1 # Selected - - return -1 # Still active - - -if __name__ == "__main__": - from openpilot.system.ui.lib.application import gui_app - - gui_app.init_window("Multi Option Dialog Example") - options = [f"Option {i}" for i in range(1, 11)] - dialog = MultiOptionDialog("Choose an option", options, options[0]) - - for _ in gui_app.render(): - result = dialog.render(rl.Rectangle(100, 100, 1024, 800)) - if result >= 0: - print(f"Selected: {dialog.selection}" if result > 0 else "Canceled") - break + return -1 diff --git a/third_party/mapd_pfeiferj/README.md b/third_party/mapd_pfeiferj/README.md new file mode 100644 index 0000000000..a814f11260 --- /dev/null +++ b/third_party/mapd_pfeiferj/README.md @@ -0,0 +1,2 @@ +# MAPD implementation by pfeiferj +https://github.com/pfeiferj/openpilot-mapd/releases/ diff --git a/third_party/mapd_pfeiferj/mapd b/third_party/mapd_pfeiferj/mapd new file mode 100755 index 0000000000..d24d9b757f Binary files /dev/null and b/third_party/mapd_pfeiferj/mapd differ diff --git a/third_party/raylib/build.sh b/third_party/raylib/build.sh index 21c9af1c0b..9e85f90df9 100755 --- a/third_party/raylib/build.sh +++ b/third_party/raylib/build.sh @@ -30,7 +30,7 @@ fi cd raylib_repo -COMMIT="66030a7de62c9e1ee8ab30a1d657a740333bb4f2" +COMMIT=${1:-66030a7de62c9e1ee8ab30a1d657a740333bb4f2} git fetch origin $COMMIT git reset --hard $COMMIT git clean -xdff . diff --git a/tools/README.md b/tools/README.md index 69b0e025e8..d52c8f4522 100644 --- a/tools/README.md +++ b/tools/README.md @@ -8,42 +8,26 @@ Most of openpilot should work natively on macOS. On Windows you can use WSL for ## Native setup on Ubuntu 24.04 and macOS +Follow these instructions for a fully managed setup experience. If you'd like to manage the dependencies yourself, just read the setup scripts in this directory. + **1. Clone openpilot** - -NOTE: This repository uses Git LFS for large files. Ensure you have [Git LFS](https://git-lfs.com/) installed and set up before cloning or working with it. - -Either do a partial clone for faster download: ``` bash -git clone --filter=blob:none --recurse-submodules --also-filter-submodules https://github.com/commaai/openpilot.git -``` - -or do a full clone: -``` bash -git clone --recurse-submodules https://github.com/commaai/openpilot.git +git clone https://github.com/commaai/openpilot.git ``` **2. Run the setup script** - ``` bash cd openpilot tools/op.sh setup ``` -**3. Git LFS** - -``` bash -git lfs pull -``` - -**4. Activate a python shell** - +**3. Activate a Python shell** Activate a shell with the Python dependencies installed: ``` bash source .venv/bin/activate ``` -**5. Build openpilot** - +**4. Build openpilot** ``` bash scons -u -j$(nproc) ``` @@ -62,8 +46,6 @@ Learn about the openpilot ecosystem and tools by playing our [CTF](/tools/CTF.md ## Directory Structure ``` -├── ubuntu_setup.sh # Setup script for Ubuntu -├── mac_setup.sh # Setup script for macOS ├── cabana/ # View and plot CAN messages from drives or in realtime ├── camerastream/ # Cameras stream over the network ├── joystick/ # Control your car with a joystick diff --git a/tools/car_porting/auto_fingerprint.py b/tools/car_porting/auto_fingerprint.py index 5080293e29..abae8d9f17 100755 --- a/tools/car_porting/auto_fingerprint.py +++ b/tools/car_porting/auto_fingerprint.py @@ -2,7 +2,7 @@ import argparse from collections import defaultdict -from openpilot.selfdrive.debug.format_fingerprints import format_brand_fw_versions +from opendbc.car.debug.format_fingerprints import format_brand_fw_versions from opendbc.car.fingerprints import MIGRATION from opendbc.car.fw_versions import MODEL_TO_BRAND, match_fw_to_car diff --git a/tools/install_python_dependencies.sh b/tools/install_python_dependencies.sh index 8d4a8b6d90..cdbaca32cf 100755 --- a/tools/install_python_dependencies.sh +++ b/tools/install_python_dependencies.sh @@ -23,8 +23,8 @@ echo "installing python packages..." uv sync --frozen --all-extras source .venv/bin/activate -echo "PYTHONPATH=${PWD}" > "$ROOT"/.env if [[ "$(uname)" == 'Darwin' ]]; then + touch "$ROOT"/.env echo "# msgq doesn't work on mac" >> "$ROOT"/.env echo "export ZMQ=1" >> "$ROOT"/.env echo "export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES" >> "$ROOT"/.env diff --git a/tools/lib/framereader.py b/tools/lib/framereader.py index 06e2345abf..8f7f723c28 100644 --- a/tools/lib/framereader.py +++ b/tools/lib/framereader.py @@ -1,62 +1,68 @@ -import json import os -import pickle -import struct import subprocess -import threading -from enum import IntEnum -from functools import wraps +import json +from collections.abc import Iterator +from collections import OrderedDict import numpy as np -from lru import LRU - -import _io -from openpilot.tools.lib.cache import cache_path_for_file_path, DEFAULT_CACHE_DIR +from openpilot.tools.lib.filereader import FileReader, resolve_name from openpilot.tools.lib.exceptions import DataUnreadableError from openpilot.tools.lib.vidindex import hevc_index -from openpilot.common.file_helpers import atomic_write_in_dir -from openpilot.tools.lib.filereader import FileReader, resolve_name HEVC_SLICE_B = 0 HEVC_SLICE_P = 1 HEVC_SLICE_I = 2 -class GOPReader: - def get_gop(self, num): - # returns (start_frame_num, num_frames, frames_to_skip, gop_data) - raise NotImplementedError +class LRUCache: + def __init__(self, capacity: int): + self._cache: OrderedDict = OrderedDict() + self.capacity = capacity + + def __getitem__(self, key): + self._cache.move_to_end(key) + return self._cache[key] + + def __setitem__(self, key, value): + self._cache[key] = value + if len(self._cache) > self.capacity: + self._cache.popitem(last=False) + + def __contains__(self, key): + return key in self._cache -class DoNothingContextManager: - def __enter__(self): - return self - - def __exit__(self, *x): - pass - - -class FrameType(IntEnum): - raw = 1 - h265_stream = 2 - - -def fingerprint_video(fn): +def assert_hvec(fn: str) -> None: with FileReader(fn) as f: header = f.read(4) if len(header) == 0: raise DataUnreadableError(f"{fn} is empty") - elif header == b"\x00\xc0\x12\x00": - return FrameType.raw elif header == b"\x00\x00\x00\x01": - if 'hevc' in fn: - return FrameType.h265_stream - else: + if 'hevc' not in fn: raise NotImplementedError(fn) - else: - raise NotImplementedError(fn) +def decompress_video_data(rawdat, w, h, pix_fmt="rgb24", vid_fmt='hevc') -> np.ndarray: + threads = os.getenv("FFMPEG_THREADS", "0") + args = ["ffmpeg", "-v", "quiet", + "-threads", threads, + "-c:v", "hevc", + "-vsync", "0", + "-f", vid_fmt, + "-flags2", "showall", + "-i", "-", + "-f", "rawvideo", + "-pix_fmt", pix_fmt, + "-"] + dat = subprocess.check_output(args, input=rawdat) + + if pix_fmt == "rgb24": + ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, h, w, 3) + elif pix_fmt in ["nv12", "yuv420p"]: + ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, (h*w*3//2)) + else: + raise NotImplementedError(f"Unsupported pixel format: {pix_fmt}") + return ret def ffprobe(fn, fmt=None): fn = resolve_name(fn) @@ -70,42 +76,21 @@ def ffprobe(fn, fmt=None): ffprobe_output = subprocess.check_output(cmd, input=f.read(4096)) except subprocess.CalledProcessError as e: raise DataUnreadableError(fn) from e - return json.loads(ffprobe_output) +def get_index_data(fn: str, index_data: dict|None = None): + if index_data is None: + index_data = get_video_index(fn) + if index_data is None: + raise DataUnreadableError(f"Failed to index {fn!r}") + stream = index_data["probe"]["streams"][0] + return index_data["index"], index_data["global_prefix"], stream["width"], stream["height"] -def cache_fn(func): - @wraps(func) - def cache_inner(fn, *args, **kwargs): - if kwargs.pop('no_cache', None): - cache_path = None - else: - cache_dir = kwargs.pop('cache_dir', DEFAULT_CACHE_DIR) - cache_path = cache_path_for_file_path(fn, cache_dir) - - if cache_path and os.path.exists(cache_path): - with open(cache_path, "rb") as cache_file: - cache_value = pickle.load(cache_file) - else: - cache_value = func(fn, *args, **kwargs) - if cache_path: - with atomic_write_in_dir(cache_path, mode="wb", overwrite=True) as cache_file: - pickle.dump(cache_value, cache_file, -1) - - return cache_value - - return cache_inner - - -@cache_fn -def index_stream(fn, ft): - if ft != FrameType.h265_stream: - raise NotImplementedError("Only h265 supported") - +def get_video_index(fn): + assert_hvec(fn) frame_types, dat_len, prefix = hevc_index(fn) index = np.array(frame_types + [(0xFFFFFFFF, dat_len)], dtype=np.uint32) probe = ffprobe(fn, "hevc") - return { 'index': index, 'global_prefix': prefix, @@ -113,447 +98,75 @@ def index_stream(fn, ft): } -def get_video_index(fn, frame_type, cache_dir=DEFAULT_CACHE_DIR): - return index_stream(fn, frame_type, cache_dir=cache_dir) - -def read_file_check_size(f, sz, cookie): - buff = bytearray(sz) - bytes_read = f.readinto(buff) - assert bytes_read == sz, (bytes_read, sz) - return buff - - -def rgb24toyuv(rgb): - yuv_from_rgb = np.array([[ 0.299 , 0.587 , 0.114 ], - [-0.14714119, -0.28886916, 0.43601035 ], - [ 0.61497538, -0.51496512, -0.10001026 ]]) - img = np.dot(rgb.reshape(-1, 3), yuv_from_rgb.T).reshape(rgb.shape) - - - - ys = img[:, :, 0] - us = (img[::2, ::2, 1] + img[1::2, ::2, 1] + img[::2, 1::2, 1] + img[1::2, 1::2, 1]) / 4 + 128 - vs = (img[::2, ::2, 2] + img[1::2, ::2, 2] + img[::2, 1::2, 2] + img[1::2, 1::2, 2]) / 4 + 128 - - return ys, us, vs - - -def rgb24toyuv420(rgb): - ys, us, vs = rgb24toyuv(rgb) - - y_len = rgb.shape[0] * rgb.shape[1] - uv_len = y_len // 4 - - yuv420 = np.empty(y_len + 2 * uv_len, dtype=rgb.dtype) - yuv420[:y_len] = ys.reshape(-1) - yuv420[y_len:y_len + uv_len] = us.reshape(-1) - yuv420[y_len + uv_len:y_len + 2 * uv_len] = vs.reshape(-1) - - return yuv420.clip(0, 255).astype('uint8') - - -def rgb24tonv12(rgb): - ys, us, vs = rgb24toyuv(rgb) - - y_len = rgb.shape[0] * rgb.shape[1] - uv_len = y_len // 4 - - nv12 = np.empty(y_len + 2 * uv_len, dtype=rgb.dtype) - nv12[:y_len] = ys.reshape(-1) - nv12[y_len::2] = us.reshape(-1) - nv12[y_len+1::2] = vs.reshape(-1) - - return nv12.clip(0, 255).astype('uint8') - - -def decompress_video_data(rawdat, vid_fmt, w, h, pix_fmt): - threads = os.getenv("FFMPEG_THREADS", "0") - cuda = os.getenv("FFMPEG_CUDA", "0") == "1" - args = ["ffmpeg", "-v", "quiet", - "-threads", threads, - "-hwaccel", "none" if not cuda else "cuda", - "-c:v", "hevc", - "-vsync", "0", - "-f", vid_fmt, - "-flags2", "showall", - "-i", "-", - "-threads", threads, - "-f", "rawvideo", - "-pix_fmt", pix_fmt, - "-"] - dat = subprocess.check_output(args, input=rawdat) - - if pix_fmt == "rgb24": - ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, h, w, 3) - elif pix_fmt == "nv12": - ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, (h*w*3//2)) - elif pix_fmt == "yuv420p": - ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, (h*w*3//2)) - elif pix_fmt == "yuv444p": - ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, 3, h, w) - else: - raise NotImplementedError - - return ret - - -class BaseFrameReader: - # properties: frame_type, frame_count, w, h - - def __enter__(self): - return self - - def __exit__(self, *args): - self.close() - - def close(self): - pass - - def get(self, num, count=1, pix_fmt="yuv420p"): - raise NotImplementedError - - -def FrameReader(fn, cache_dir=DEFAULT_CACHE_DIR, readahead=False, readbehind=False, index_data=None): - frame_type = fingerprint_video(fn) - if frame_type == FrameType.raw: - return RawFrameReader(fn) - elif frame_type in (FrameType.h265_stream,): - if not index_data: - index_data = get_video_index(fn, frame_type, cache_dir) - return StreamFrameReader(fn, frame_type, index_data, readahead=readahead, readbehind=readbehind) - else: - raise NotImplementedError(frame_type) - - -class RawData: - def __init__(self, f): - self.f = _io.FileIO(f, 'rb') - self.lenn = struct.unpack("I", self.f.read(4))[0] - self.count = os.path.getsize(f) / (self.lenn+4) - - def read(self, i): - self.f.seek((self.lenn+4)*i + 4) - return self.f.read(self.lenn) - - -class RawFrameReader(BaseFrameReader): - def __init__(self, fn): - # raw camera +class FfmpegDecoder: + def __init__(self, fn: str, index_data: dict|None = None, + pix_fmt: str = "rgb24"): self.fn = fn - self.frame_type = FrameType.raw - self.rawfile = RawData(self.fn) - self.frame_count = self.rawfile.count - self.w, self.h = 640, 480 - - def load_and_debayer(self, img): - img = np.frombuffer(img, dtype='uint8').reshape(960, 1280) - cimg = np.dstack([img[0::2, 1::2], ((img[0::2, 0::2].astype("uint16") + img[1::2, 1::2].astype("uint16")) >> 1).astype("uint8"), img[1::2, 0::2]]) - return cimg - - def get(self, num, count=1, pix_fmt="yuv420p"): - assert self.frame_count is not None - assert num+count <= self.frame_count - - if pix_fmt not in ("nv12", "yuv420p", "rgb24"): - raise ValueError(f"Unsupported pixel format {pix_fmt!r}") - - app = [] - for i in range(num, num+count): - dat = self.rawfile.read(i) - rgb_dat = self.load_and_debayer(dat) - if pix_fmt == "rgb24": - app.append(rgb_dat) - elif pix_fmt == "nv12": - app.append(rgb24tonv12(rgb_dat)) - elif pix_fmt == "yuv420p": - app.append(rgb24toyuv420(rgb_dat)) - else: - raise NotImplementedError - - return app - - -class VideoStreamDecompressor: - def __init__(self, fn, vid_fmt, w, h, pix_fmt): - self.fn = fn - self.vid_fmt = vid_fmt - self.w = w - self.h = h + self.index, self.prefix, self.w, self.h = get_index_data(fn, index_data) + self.frame_count = len(self.index) - 1 # sentinel row at the end + self.iframes = np.where(self.index[:, 0] == HEVC_SLICE_I)[0] self.pix_fmt = pix_fmt - if pix_fmt in ("nv12", "yuv420p"): - self.out_size = w*h*3//2 # yuv420p - elif pix_fmt in ("rgb24", "yuv444p"): - self.out_size = w*h*3 - else: - raise NotImplementedError + def _gop_bounds(self, frame_idx: int): + f_b = frame_idx + while f_b > 0 and self.index[f_b, 0] != HEVC_SLICE_I: + f_b -= 1 + f_e = frame_idx + 1 + while f_e < self.frame_count and self.index[f_e, 0] != HEVC_SLICE_I: + f_e += 1 + return f_b, f_e, self.index[f_b, 1], self.index[f_e, 1] - self.proc = None - self.t = threading.Thread(target=self.write_thread) - self.t.daemon = True + def _decode_gop(self, raw: bytes) -> Iterator[np.ndarray]: + yield from decompress_video_data(raw, self.w, self.h, self.pix_fmt) - def write_thread(self): - try: + def get_gop_start(self, frame_idx: int): + return self.iframes[np.searchsorted(self.iframes, frame_idx, side="right") - 1] + + def get_iterator(self, start_fidx: int = 0, end_fidx: int|None = None, + frame_skip: int = 1) -> Iterator[tuple[int, np.ndarray]]: + end_fidx = end_fidx or self.frame_count + fidx = start_fidx + while fidx < end_fidx: + f_b, f_e, off_b, off_e = self._gop_bounds(fidx) with FileReader(self.fn) as f: - while True: - r = f.read(1024*1024) - if len(r) == 0: - break - self.proc.stdin.write(r) - except BrokenPipeError: - pass - finally: - self.proc.stdin.close() + f.seek(off_b) + raw = self.prefix + f.read(off_e - off_b) + # number of frames to discard inside this GOP before the wanted one + for i, frm in enumerate(decompress_video_data(raw, self.w, self.h, self.pix_fmt)): + fidx = f_b + i + if fidx >= end_fidx: + return + elif fidx >= start_fidx and (fidx - start_fidx) % frame_skip == 0: + yield fidx, frm + fidx += 1 - def read(self): - threads = os.getenv("FFMPEG_THREADS", "0") - cuda = os.getenv("FFMPEG_CUDA", "0") == "1" - cmd = [ - "ffmpeg", - "-threads", threads, - "-hwaccel", "none" if not cuda else "cuda", - "-c:v", "hevc", - # "-avioflags", "direct", - "-analyzeduration", "0", - "-probesize", "32", - "-flush_packets", "0", - # "-fflags", "nobuffer", - "-vsync", "0", - "-f", self.vid_fmt, - "-i", "pipe:0", - "-threads", threads, - "-f", "rawvideo", - "-pix_fmt", self.pix_fmt, - "pipe:1" - ] - self.proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - try: - self.t.start() +def FrameIterator(fn: str, index_data: dict|None=None, + pix_fmt: str = "rgb24", + start_fidx:int=0, end_fidx=None, frame_skip:int=1) -> Iterator[np.ndarray]: + dec = FfmpegDecoder(fn, pix_fmt=pix_fmt, index_data=index_data) + for _, frame in dec.get_iterator(start_fidx=start_fidx, end_fidx=end_fidx, frame_skip=frame_skip): + yield frame - while True: - dat = self.proc.stdout.read(self.out_size) - if len(dat) == 0: - break - assert len(dat) == self.out_size - if self.pix_fmt == "rgb24": - ret = np.frombuffer(dat, dtype=np.uint8).reshape((self.h, self.w, 3)) - elif self.pix_fmt == "yuv420p": - ret = np.frombuffer(dat, dtype=np.uint8) - elif self.pix_fmt == "nv12": - ret = np.frombuffer(dat, dtype=np.uint8) - elif self.pix_fmt == "yuv444p": - ret = np.frombuffer(dat, dtype=np.uint8).reshape((3, self.h, self.w)) - else: - raise RuntimeError(f"unknown pix_fmt: {self.pix_fmt}") - yield ret +class FrameReader: + def __init__(self, fn: str, index_data: dict|None = None, + cache_size: int = 30, pix_fmt: str = "rgb24"): + self.decoder = FfmpegDecoder(fn, index_data, pix_fmt) + self.iframes = self.decoder.iframes + self._cache: LRUCache = LRUCache(cache_size) + self.w, self.h, self.frame_count, = self.decoder.w, self.decoder.h, self.decoder.frame_count + self.pix_fmt = pix_fmt - result_code = self.proc.wait() - assert result_code == 0, result_code - finally: - self.proc.kill() - self.t.join() + self.it: Iterator[tuple[int, np.ndarray]] | None = None + self.fidx = -1 -class StreamGOPReader(GOPReader): - def __init__(self, fn, frame_type, index_data): - assert frame_type == FrameType.h265_stream - - self.fn = fn - - self.frame_type = frame_type - self.frame_count = None - self.w, self.h = None, None - - self.prefix = None - self.index = None - - self.index = index_data['index'] - self.prefix = index_data['global_prefix'] - probe = index_data['probe'] - - self.prefix_frame_data = None - self.num_prefix_frames = 0 - self.vid_fmt = "hevc" - - i = 0 - while i < self.index.shape[0] and self.index[i, 0] != HEVC_SLICE_I: - i += 1 - self.first_iframe = i - - assert self.first_iframe == 0 - - self.frame_count = len(self.index) - 1 - - self.w = probe['streams'][0]['width'] - self.h = probe['streams'][0]['height'] - - def _lookup_gop(self, num): - frame_b = num - while frame_b > 0 and self.index[frame_b, 0] != HEVC_SLICE_I: - frame_b -= 1 - - frame_e = num + 1 - while frame_e < (len(self.index) - 1) and self.index[frame_e, 0] != HEVC_SLICE_I: - frame_e += 1 - - offset_b = self.index[frame_b, 1] - offset_e = self.index[frame_e, 1] - - return (frame_b, frame_e, offset_b, offset_e) - - def get_gop(self, num): - frame_b, frame_e, offset_b, offset_e = self._lookup_gop(num) - assert frame_b <= num < frame_e - - num_frames = frame_e - frame_b - - with FileReader(self.fn) as f: - f.seek(offset_b) - rawdat = f.read(offset_e - offset_b) - - if num < self.first_iframe: - assert self.prefix_frame_data - rawdat = self.prefix_frame_data + rawdat - - rawdat = self.prefix + rawdat - - skip_frames = 0 - if num < self.first_iframe: - skip_frames = self.num_prefix_frames - - return frame_b, num_frames, skip_frames, rawdat - - -class GOPFrameReader(BaseFrameReader): - #FrameReader with caching and readahead for formats that are group-of-picture based - - def __init__(self, readahead=False, readbehind=False): - self.open_ = True - - self.readahead = readahead - self.readbehind = readbehind - self.frame_cache = LRU(64) - - if self.readahead: - self.cache_lock = threading.RLock() - self.readahead_last = None - self.readahead_len = 30 - self.readahead_c = threading.Condition() - self.readahead_thread = threading.Thread(target=self._readahead_thread) - self.readahead_thread.daemon = True - self.readahead_thread.start() - else: - self.cache_lock = DoNothingContextManager() - - def close(self): - if not self.open_: - return - self.open_ = False - - if self.readahead: - self.readahead_c.acquire() - self.readahead_c.notify() - self.readahead_c.release() - self.readahead_thread.join() - - def _readahead_thread(self): - while True: - self.readahead_c.acquire() - try: - if not self.open_: - break - self.readahead_c.wait() - finally: - self.readahead_c.release() - if not self.open_: - break - assert self.readahead_last - num, pix_fmt = self.readahead_last - - if self.readbehind: - for k in range(num - 1, max(0, num - self.readahead_len), -1): - self._get_one(k, pix_fmt) - else: - for k in range(num, min(self.frame_count, num + self.readahead_len)): - self._get_one(k, pix_fmt) - - def _get_one(self, num, pix_fmt): - assert num < self.frame_count - - if (num, pix_fmt) in self.frame_cache: - return self.frame_cache[(num, pix_fmt)] - - with self.cache_lock: - if (num, pix_fmt) in self.frame_cache: - return self.frame_cache[(num, pix_fmt)] - - frame_b, num_frames, skip_frames, rawdat = self.get_gop(num) - - ret = decompress_video_data(rawdat, self.vid_fmt, self.w, self.h, pix_fmt) - ret = ret[skip_frames:] - # assert ret.shape[0] == num_frames - - for i in range(ret.shape[0]): - self.frame_cache[(frame_b+i, pix_fmt)] = ret[i] - - return self.frame_cache[(num, pix_fmt)] - - def get(self, num, count=1, pix_fmt="yuv420p"): - assert self.frame_count is not None - - if num + count > self.frame_count: - raise ValueError(f"{num + count} > {self.frame_count}") - - if pix_fmt not in ("nv12", "yuv420p", "rgb24", "yuv444p"): - raise ValueError(f"Unsupported pixel format {pix_fmt!r}") - - ret = [self._get_one(num + i, pix_fmt) for i in range(count)] - - if self.readahead: - self.readahead_last = (num+count, pix_fmt) - self.readahead_c.acquire() - self.readahead_c.notify() - self.readahead_c.release() - - return ret - - -class StreamFrameReader(StreamGOPReader, GOPFrameReader): - def __init__(self, fn, frame_type, index_data, readahead=False, readbehind=False): - StreamGOPReader.__init__(self, fn, frame_type, index_data) - GOPFrameReader.__init__(self, readahead, readbehind) - - -def GOPFrameIterator(gop_reader, pix_fmt): - dec = VideoStreamDecompressor(gop_reader.fn, gop_reader.vid_fmt, gop_reader.w, gop_reader.h, pix_fmt) - yield from dec.read() - - -def FrameIterator(fn, pix_fmt, **kwargs): - fr = FrameReader(fn, **kwargs) - if isinstance(fr, GOPReader): - yield from GOPFrameIterator(fr, pix_fmt) - else: - for i in range(fr.frame_count): - yield fr.get(i, pix_fmt=pix_fmt)[0] - - -class NumpyFrameReader: - def __init__(self, name, w, h, cache_size): - self.name = name - self.pos = -1 - self.frames = None - self.w = w - self.h = h - self.cache_size = cache_size - - def close(self): - pass - - def get(self, num, count=1, pix_fmt="nv12"): - num -= 1 - q = num // self.cache_size - if q != self.pos: - del self.frames - self.pos = q - self.frames = np.load(f'{self.name}_{self.pos}.npy') - return [self.frames[num % self.cache_size]] + def get(self, fidx:int): + if fidx in self._cache: # If frame is cached, return it + return self._cache[fidx] + read_start = self.decoder.get_gop_start(fidx) + if not self.it or fidx < self.fidx or read_start != self.decoder.get_gop_start(self.fidx): # If the frame is in a different GOP, reset the iterator + self.it = self.decoder.get_iterator(read_start) + self.fidx = -1 + while self.fidx < fidx: + self.fidx, frame = next(self.it) + self._cache[self.fidx] = frame + return self._cache[fidx] diff --git a/tools/lib/tests/test_readers.py b/tools/lib/tests/test_readers.py deleted file mode 100644 index 624531a1a8..0000000000 --- a/tools/lib/tests/test_readers.py +++ /dev/null @@ -1,63 +0,0 @@ -import pytest -import requests -import tempfile - -from collections import defaultdict -import numpy as np -from openpilot.tools.lib.framereader import FrameReader -from openpilot.tools.lib.logreader import LogReader - - -class TestReaders: - @pytest.mark.skip("skip for bandwidth reasons") - def test_logreader(self): - def _check_data(lr): - hist = defaultdict(int) - for l in lr: - hist[l.which()] += 1 - - assert hist['carControl'] == 6000 - assert hist['logMessage'] == 6857 - - with tempfile.NamedTemporaryFile(suffix=".bz2") as fp: - r = requests.get("https://github.com/commaai/comma2k19/blob/master/Example_1/b0c9d2329ad1606b%7C2018-08-02--08-34-47/40/raw_log.bz2?raw=true", timeout=10) - fp.write(r.content) - fp.flush() - - lr_file = LogReader(fp.name) - _check_data(lr_file) - - lr_url = LogReader("https://github.com/commaai/comma2k19/blob/master/Example_1/b0c9d2329ad1606b%7C2018-08-02--08-34-47/40/raw_log.bz2?raw=true") - _check_data(lr_url) - - @pytest.mark.skip("skip for bandwidth reasons") - def test_framereader(self): - def _check_data(f): - assert f.frame_count == 1200 - assert f.w == 1164 - assert f.h == 874 - - frame_first_30 = f.get(0, 30) - assert len(frame_first_30) == 30 - - print(frame_first_30[15]) - - print("frame_0") - frame_0 = f.get(0, 1) - frame_15 = f.get(15, 1) - - print(frame_15[0]) - - assert np.all(frame_first_30[0] == frame_0[0]) - assert np.all(frame_first_30[15] == frame_15[0]) - - with tempfile.NamedTemporaryFile(suffix=".hevc") as fp: - r = requests.get("https://github.com/commaai/comma2k19/blob/master/Example_1/b0c9d2329ad1606b%7C2018-08-02--08-34-47/40/video.hevc?raw=true", timeout=10) - fp.write(r.content) - fp.flush() - - fr_file = FrameReader(fp.name) - _check_data(fr_file) - - fr_url = FrameReader("https://github.com/commaai/comma2k19/blob/master/Example_1/b0c9d2329ad1606b%7C2018-08-02--08-34-47/40/video.hevc?raw=true") - _check_data(fr_url) diff --git a/tools/replay/unlog_ci_segment.py b/tools/replay/unlog_ci_segment.py index adc0b19e9b..6ddc1b298a 100755 --- a/tools/replay/unlog_ci_segment.py +++ b/tools/replay/unlog_ci_segment.py @@ -47,8 +47,8 @@ def replay(route, segment, loop): if w == 'roadCameraState': try: - img = fr.get(frame_idx[msg.roadCameraState.frameId], pix_fmt="rgb24") - img = img[0][:, :, ::-1] # Convert RGB to BGR, which is what the camera outputs + img = fr.get(frame_idx[msg.roadCameraState.frameId]) + img = img[:, ::-1] # Convert RGB to BGR, which is what the camera outputs msg.roadCameraState.image = img.flatten().tobytes() except (KeyError, ValueError): pass diff --git a/tools/scripts/fetch_image_from_route.py b/tools/scripts/fetch_image_from_route.py index 521f1597ec..77bf48f946 100755 --- a/tools/scripts/fetch_image_from_route.py +++ b/tools/scripts/fetch_image_from_route.py @@ -37,7 +37,7 @@ fr = FrameReader(segments[segment]) if frame >= fr.frame_count: raise Exception("frame {frame} not found, got {fr.frame_count} frames") -im = Image.fromarray(fr.get(frame, count=1, pix_fmt="rgb24")[0]) +im = Image.fromarray(fr.get(frame)) fn = f"uxxx_{route.replace('|', '_')}_{segment}_{frame}.png" im.save(fn) print(f"saved {fn}") diff --git a/tools/webcam/README.md b/tools/webcam/README.md index f4b46d1f66..67ad2cc8cb 100644 --- a/tools/webcam/README.md +++ b/tools/webcam/README.md @@ -1,22 +1,22 @@ # Run openpilot with webcam on PC What's needed: -- Ubuntu 24.04 ([WSL2 is not supported](https://github.com/commaai/openpilot/issues/34216)) +- Ubuntu 24.04 ([WSL2 is not supported](https://github.com/commaai/openpilot/issues/34216)) or macOS - GPU (recommended) -- Two USB webcams, at least 720p and 78 degrees FOV (e.g. Logitech C920/C615) -- [Car harness](https://comma.ai/shop/products/comma-car-harness) with black panda to connect to your car -- [Panda paw](https://comma.ai/shop/products/panda-paw) or USB-A to USB-A cable to connect panda to your computer -That's it! +- One USB webcam, at least 720p and 78 degrees FOV (e.g. Logitech C920/C615, NexiGo N60) +- [Car harness](https://comma.ai/shop/products/comma-car-harness) +- [panda](https://comma.ai/shop/panda) +- USB-A to USB-A cable to connect panda to your computer ## Setup openpilot - Follow [this readme](../README.md) to install and build the requirements -- Install OpenCL Driver +- Install OpenCL Driver (Ubuntu) ``` sudo apt install pocl-opencl-icd ``` ## Connect the hardware -- Connect the road facing camera first, then the driver facing camera +- Connect the camera first - Connect your computer to panda ## GO @@ -24,12 +24,12 @@ sudo apt install pocl-opencl-icd USE_WEBCAM=1 system/manager/manager.py ``` - Start the car, then the UI should show the road webcam's view -- Adjust and secure the webcams. +- Adjust and secure the webcam - Finish calibration and engage! ## Specify Cameras -Use the `ROAD_CAM`, `DRIVER_CAM`, and optional `WIDE_CAM` environment variables to specify which camera is which (ie. `DRIVER_CAM=2` uses `/dev/video2` for the driver-facing camera): +Use the `ROAD_CAM` (default 0) and optional `DRIVER_CAM`, `WIDE_CAM` environment variables to specify which camera is which (ie. `ROAD_CAM=1` uses `/dev/video1`, on Ubuntu, for the road camera): ``` -USE_WEBCAM=1 ROAD_CAM=4 WIDE_CAM=6 system/manager/manager.py +USE_WEBCAM=1 ROAD_CAM=1 system/manager/manager.py ``` diff --git a/tools/webcam/camera.py b/tools/webcam/camera.py index 8358846f3a..45f9379038 100644 --- a/tools/webcam/camera.py +++ b/tools/webcam/camera.py @@ -1,4 +1,5 @@ import av +import cv2 as cv class Camera: def __init__(self, cam_type_state, stream_type, camera_id): @@ -10,11 +11,9 @@ class Camera: self.stream_type = stream_type self.cur_frame_id = 0 - self.container = av.open(camera_id) - assert self.container.streams.video, f"Can't open video stream for camera {camera_id}" - self.video_stream = self.container.streams.video[0] - self.W = self.video_stream.codec_context.width - self.H = self.video_stream.codec_context.height + self.cap = cv.VideoCapture(camera_id) + self.W = self.cap.get(cv.CAP_PROP_FRAME_WIDTH) + self.H = self.cap.get(cv.CAP_PROP_FRAME_HEIGHT) @classmethod def bgr2nv12(self, bgr): @@ -22,8 +21,10 @@ class Camera: return frame.reformat(format='nv12').to_ndarray() def read_frames(self): - for frame in self.container.decode(self.video_stream): - img = frame.to_rgb().to_ndarray()[:,:, ::-1] # convert to bgr24 - yuv = Camera.bgr2nv12(img) + while True: + ret, frame = self.cap.read() + if not ret: + break + yuv = Camera.bgr2nv12(frame) yield yuv.data.tobytes() - self.container.close() + self.cap.release() diff --git a/tools/webcam/camerad.py b/tools/webcam/camerad.py index a0916ed5ee..cb096dc00b 100755 --- a/tools/webcam/camerad.py +++ b/tools/webcam/camerad.py @@ -9,14 +9,19 @@ from cereal import messaging from openpilot.tools.webcam.camera import Camera from openpilot.common.realtime import Ratekeeper +ROAD_CAM = os.getenv("ROAD_CAM", "0") WIDE_CAM = os.getenv("WIDE_CAM") +DRIVER_CAM = os.getenv("DRIVER_CAM") + CameraType = namedtuple("CameraType", ["msg_name", "stream_type", "cam_id"]) + CAMERAS = [ - CameraType("roadCameraState", VisionStreamType.VISION_STREAM_ROAD, os.getenv("ROAD_CAM", "0")), - CameraType("driverCameraState", VisionStreamType.VISION_STREAM_DRIVER, os.getenv("DRIVER_CAM", "2")), + CameraType("roadCameraState", VisionStreamType.VISION_STREAM_ROAD, ROAD_CAM) ] if WIDE_CAM: CAMERAS.append(CameraType("wideRoadCameraState", VisionStreamType.VISION_STREAM_WIDE_ROAD, WIDE_CAM)) +if DRIVER_CAM: + CAMERAS.append(CameraType("driverCameraState", VisionStreamType.VISION_STREAM_DRIVER, DRIVER_CAM)) class Camerad: def __init__(self): diff --git a/uv.lock b/uv.lock index 2f8cf68094..530db4ec9d 100644 --- a/uv.lock +++ b/uv.lock @@ -21,7 +21,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.11.18" +version = "3.12.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -32,40 +32,42 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/e7/fa1a8c00e2c54b05dc8cb5d1439f627f7c267874e3f7bb047146116020f9/aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a", size = 7678653, upload-time = "2025-04-21T09:43:09.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload-time = "2025-06-14T15:15:41.354Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/10/fd9ee4f9e042818c3c2390054c08ccd34556a3cb209d83285616434cf93e/aiohttp-3.11.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:427fdc56ccb6901ff8088544bde47084845ea81591deb16f957897f0f0ba1be9", size = 712088, upload-time = "2025-04-21T09:40:55.776Z" }, - { url = "https://files.pythonhosted.org/packages/22/eb/6a77f055ca56f7aae2cd2a5607a3c9e7b9554f1497a069dcfcb52bfc9540/aiohttp-3.11.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c828b6d23b984255b85b9b04a5b963a74278b7356a7de84fda5e3b76866597b", size = 471450, upload-time = "2025-04-21T09:40:57.301Z" }, - { url = "https://files.pythonhosted.org/packages/78/dc/5f3c0d27c91abf0bb5d103e9c9b0ff059f60cf6031a5f06f456c90731f42/aiohttp-3.11.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c2eaa145bb36b33af1ff2860820ba0589e165be4ab63a49aebfd0981c173b66", size = 457836, upload-time = "2025-04-21T09:40:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/49/7b/55b65af9ef48b9b811c91ff8b5b9de9650c71147f10523e278d297750bc8/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d518ce32179f7e2096bf4e3e8438cf445f05fedd597f252de9f54c728574756", size = 1690978, upload-time = "2025-04-21T09:41:00.795Z" }, - { url = "https://files.pythonhosted.org/packages/a2/5a/3f8938c4f68ae400152b42742653477fc625d6bfe02e764f3521321c8442/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700055a6e05c2f4711011a44364020d7a10fbbcd02fbf3e30e8f7e7fddc8717", size = 1745307, upload-time = "2025-04-21T09:41:02.89Z" }, - { url = "https://files.pythonhosted.org/packages/b4/42/89b694a293333ef6f771c62da022163bcf44fb03d4824372d88e3dc12530/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bd1cde83e4684324e6ee19adfc25fd649d04078179890be7b29f76b501de8e4", size = 1780692, upload-time = "2025-04-21T09:41:04.461Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ce/1a75384e01dd1bf546898b6062b1b5f7a59b6692ef802e4dd6db64fed264/aiohttp-3.11.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73b8870fe1c9a201b8c0d12c94fe781b918664766728783241a79e0468427e4f", size = 1676934, upload-time = "2025-04-21T09:41:06.728Z" }, - { url = "https://files.pythonhosted.org/packages/a5/31/442483276e6c368ab5169797d9873b5875213cbcf7e74b95ad1c5003098a/aiohttp-3.11.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25557982dd36b9e32c0a3357f30804e80790ec2c4d20ac6bcc598533e04c6361", size = 1621190, upload-time = "2025-04-21T09:41:08.293Z" }, - { url = "https://files.pythonhosted.org/packages/7b/83/90274bf12c079457966008a58831a99675265b6a34b505243e004b408934/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e889c9df381a2433802991288a61e5a19ceb4f61bd14f5c9fa165655dcb1fd1", size = 1658947, upload-time = "2025-04-21T09:41:11.054Z" }, - { url = "https://files.pythonhosted.org/packages/91/c1/da9cee47a0350b78fdc93670ebe7ad74103011d7778ab4c382ca4883098d/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9ea345fda05bae217b6cce2acf3682ce3b13d0d16dd47d0de7080e5e21362421", size = 1654443, upload-time = "2025-04-21T09:41:13.213Z" }, - { url = "https://files.pythonhosted.org/packages/c9/f2/73cbe18dc25d624f79a09448adfc4972f82ed6088759ddcf783cd201956c/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9f26545b9940c4b46f0a9388fd04ee3ad7064c4017b5a334dd450f616396590e", size = 1644169, upload-time = "2025-04-21T09:41:14.827Z" }, - { url = "https://files.pythonhosted.org/packages/5b/32/970b0a196c4dccb1b0cfa5b4dc3b20f63d76f1c608f41001a84b2fd23c3d/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3a621d85e85dccabd700294494d7179ed1590b6d07a35709bb9bd608c7f5dd1d", size = 1728532, upload-time = "2025-04-21T09:41:17.168Z" }, - { url = "https://files.pythonhosted.org/packages/0b/50/b1dc810a41918d2ea9574e74125eb053063bc5e14aba2d98966f7d734da0/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c23fd8d08eb9c2af3faeedc8c56e134acdaf36e2117ee059d7defa655130e5f", size = 1750310, upload-time = "2025-04-21T09:41:19.353Z" }, - { url = "https://files.pythonhosted.org/packages/95/24/39271f5990b35ff32179cc95537e92499d3791ae82af7dcf562be785cd15/aiohttp-3.11.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9e6b0e519067caa4fd7fb72e3e8002d16a68e84e62e7291092a5433763dc0dd", size = 1691580, upload-time = "2025-04-21T09:41:21.868Z" }, - { url = "https://files.pythonhosted.org/packages/6b/78/75d0353feb77f041460564f12fe58e456436bbc00cbbf5d676dbf0038cc2/aiohttp-3.11.18-cp311-cp311-win32.whl", hash = "sha256:122f3e739f6607e5e4c6a2f8562a6f476192a682a52bda8b4c6d4254e1138f4d", size = 417565, upload-time = "2025-04-21T09:41:24.78Z" }, - { url = "https://files.pythonhosted.org/packages/ed/97/b912dcb654634a813f8518de359364dfc45976f822116e725dc80a688eee/aiohttp-3.11.18-cp311-cp311-win_amd64.whl", hash = "sha256:e6f3c0a3a1e73e88af384b2e8a0b9f4fb73245afd47589df2afcab6b638fa0e6", size = 443652, upload-time = "2025-04-21T09:41:26.48Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d2/5bc436f42bf4745c55f33e1e6a2d69e77075d3e768e3d1a34f96ee5298aa/aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2", size = 706671, upload-time = "2025-04-21T09:41:28.021Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d0/2dbabecc4e078c0474abb40536bbde717fb2e39962f41c5fc7a216b18ea7/aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508", size = 466169, upload-time = "2025-04-21T09:41:29.783Z" }, - { url = "https://files.pythonhosted.org/packages/70/84/19edcf0b22933932faa6e0be0d933a27bd173da02dc125b7354dff4d8da4/aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e", size = 457554, upload-time = "2025-04-21T09:41:31.327Z" }, - { url = "https://files.pythonhosted.org/packages/32/d0/e8d1f034ae5624a0f21e4fb3feff79342ce631f3a4d26bd3e58b31ef033b/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f", size = 1690154, upload-time = "2025-04-21T09:41:33.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/de/2f9dbe2ac6f38f8495562077131888e0d2897e3798a0ff3adda766b04a34/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f", size = 1733402, upload-time = "2025-04-21T09:41:35.634Z" }, - { url = "https://files.pythonhosted.org/packages/e0/04/bd2870e1e9aef990d14b6df2a695f17807baf5c85a4c187a492bda569571/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec", size = 1783958, upload-time = "2025-04-21T09:41:37.456Z" }, - { url = "https://files.pythonhosted.org/packages/23/06/4203ffa2beb5bedb07f0da0f79b7d9039d1c33f522e0d1a2d5b6218e6f2e/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6", size = 1695288, upload-time = "2025-04-21T09:41:39.756Z" }, - { url = "https://files.pythonhosted.org/packages/30/b2/e2285dda065d9f29ab4b23d8bcc81eb881db512afb38a3f5247b191be36c/aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009", size = 1618871, upload-time = "2025-04-21T09:41:41.972Z" }, - { url = "https://files.pythonhosted.org/packages/57/e0/88f2987885d4b646de2036f7296ebea9268fdbf27476da551c1a7c158bc0/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4", size = 1646262, upload-time = "2025-04-21T09:41:44.192Z" }, - { url = "https://files.pythonhosted.org/packages/e0/19/4d2da508b4c587e7472a032290b2981f7caeca82b4354e19ab3df2f51d56/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9", size = 1677431, upload-time = "2025-04-21T09:41:46.049Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/047473ea50150a41440f3265f53db1738870b5a1e5406ece561ca61a3bf4/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb", size = 1637430, upload-time = "2025-04-21T09:41:47.973Z" }, - { url = "https://files.pythonhosted.org/packages/11/32/c6d1e3748077ce7ee13745fae33e5cb1dac3e3b8f8787bf738a93c94a7d2/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda", size = 1703342, upload-time = "2025-04-21T09:41:50.323Z" }, - { url = "https://files.pythonhosted.org/packages/c5/1d/a3b57bfdbe285f0d45572d6d8f534fd58761da3e9cbc3098372565005606/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1", size = 1740600, upload-time = "2025-04-21T09:41:52.111Z" }, - { url = "https://files.pythonhosted.org/packages/a5/71/f9cd2fed33fa2b7ce4d412fb7876547abb821d5b5520787d159d0748321d/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea", size = 1695131, upload-time = "2025-04-21T09:41:53.94Z" }, - { url = "https://files.pythonhosted.org/packages/97/97/d1248cd6d02b9de6aa514793d0dcb20099f0ec47ae71a933290116c070c5/aiohttp-3.11.18-cp312-cp312-win32.whl", hash = "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8", size = 412442, upload-time = "2025-04-21T09:41:55.689Z" }, - { url = "https://files.pythonhosted.org/packages/33/9a/e34e65506e06427b111e19218a99abf627638a9703f4b8bcc3e3021277ed/aiohttp-3.11.18-cp312-cp312-win_amd64.whl", hash = "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8", size = 439444, upload-time = "2025-04-21T09:41:57.977Z" }, + { url = "https://files.pythonhosted.org/packages/6a/65/5566b49553bf20ffed6041c665a5504fb047cefdef1b701407b8ce1a47c4/aiohttp-3.12.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c229b1437aa2576b99384e4be668af1db84b31a45305d02f61f5497cfa6f60c", size = 709401, upload-time = "2025-06-14T15:13:30.774Z" }, + { url = "https://files.pythonhosted.org/packages/14/b5/48e4cc61b54850bdfafa8fe0b641ab35ad53d8e5a65ab22b310e0902fa42/aiohttp-3.12.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04076d8c63471e51e3689c93940775dc3d12d855c0c80d18ac5a1c68f0904358", size = 481669, upload-time = "2025-06-14T15:13:32.316Z" }, + { url = "https://files.pythonhosted.org/packages/04/4f/e3f95c8b2a20a0437d51d41d5ccc4a02970d8ad59352efb43ea2841bd08e/aiohttp-3.12.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55683615813ce3601640cfaa1041174dc956d28ba0511c8cbd75273eb0587014", size = 469933, upload-time = "2025-06-14T15:13:34.104Z" }, + { url = "https://files.pythonhosted.org/packages/41/c9/c5269f3b6453b1cfbd2cfbb6a777d718c5f086a3727f576c51a468b03ae2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921bc91e602d7506d37643e77819cb0b840d4ebb5f8d6408423af3d3bf79a7b7", size = 1740128, upload-time = "2025-06-14T15:13:35.604Z" }, + { url = "https://files.pythonhosted.org/packages/6f/49/a3f76caa62773d33d0cfaa842bdf5789a78749dbfe697df38ab1badff369/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e72d17fe0974ddeae8ed86db297e23dba39c7ac36d84acdbb53df2e18505a013", size = 1688796, upload-time = "2025-06-14T15:13:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e4/556fccc4576dc22bf18554b64cc873b1a3e5429a5bdb7bbef7f5d0bc7664/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0653d15587909a52e024a261943cf1c5bdc69acb71f411b0dd5966d065a51a47", size = 1787589, upload-time = "2025-06-14T15:13:38.745Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3d/d81b13ed48e1a46734f848e26d55a7391708421a80336e341d2aef3b6db2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77b48997c66722c65e157c06c74332cdf9c7ad00494b85ec43f324e5c5a9b9a", size = 1826635, upload-time = "2025-06-14T15:13:40.733Z" }, + { url = "https://files.pythonhosted.org/packages/75/a5/472e25f347da88459188cdaadd1f108f6292f8a25e62d226e63f860486d1/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6946bae55fd36cfb8e4092c921075cde029c71c7cb571d72f1079d1e4e013bc", size = 1729095, upload-time = "2025-06-14T15:13:42.312Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fe/322a78b9ac1725bfc59dfc301a5342e73d817592828e4445bd8f4ff83489/aiohttp-3.12.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f95db8c8b219bcf294a53742c7bda49b80ceb9d577c8e7aa075612b7f39ffb7", size = 1666170, upload-time = "2025-06-14T15:13:44.884Z" }, + { url = "https://files.pythonhosted.org/packages/7a/77/ec80912270e231d5e3839dbd6c065472b9920a159ec8a1895cf868c2708e/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03d5eb3cfb4949ab4c74822fb3326cd9655c2b9fe22e4257e2100d44215b2e2b", size = 1714444, upload-time = "2025-06-14T15:13:46.401Z" }, + { url = "https://files.pythonhosted.org/packages/21/b2/fb5aedbcb2b58d4180e58500e7c23ff8593258c27c089abfbcc7db65bd40/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6383dd0ffa15515283c26cbf41ac8e6705aab54b4cbb77bdb8935a713a89bee9", size = 1709604, upload-time = "2025-06-14T15:13:48.377Z" }, + { url = "https://files.pythonhosted.org/packages/e3/15/a94c05f7c4dc8904f80b6001ad6e07e035c58a8ebfcc15e6b5d58500c858/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6548a411bc8219b45ba2577716493aa63b12803d1e5dc70508c539d0db8dbf5a", size = 1689786, upload-time = "2025-06-14T15:13:50.401Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/0d2e618388f7a7a4441eed578b626bda9ec6b5361cd2954cfc5ab39aa170/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81b0fcbfe59a4ca41dc8f635c2a4a71e63f75168cc91026c61be665945739e2d", size = 1783389, upload-time = "2025-06-14T15:13:51.945Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6b/6986d0c75996ef7e64ff7619b9b7449b1d1cbbe05c6755e65d92f1784fe9/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a83797a0174e7995e5edce9dcecc517c642eb43bc3cba296d4512edf346eee2", size = 1803853, upload-time = "2025-06-14T15:13:53.533Z" }, + { url = "https://files.pythonhosted.org/packages/21/65/cd37b38f6655d95dd07d496b6d2f3924f579c43fd64b0e32b547b9c24df5/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5734d8469a5633a4e9ffdf9983ff7cdb512524645c7a3d4bc8a3de45b935ac3", size = 1716909, upload-time = "2025-06-14T15:13:55.148Z" }, + { url = "https://files.pythonhosted.org/packages/fd/20/2de7012427dc116714c38ca564467f6143aec3d5eca3768848d62aa43e62/aiohttp-3.12.13-cp311-cp311-win32.whl", hash = "sha256:fef8d50dfa482925bb6b4c208b40d8e9fa54cecba923dc65b825a72eed9a5dbd", size = 427036, upload-time = "2025-06-14T15:13:57.076Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b6/98518bcc615ef998a64bef371178b9afc98ee25895b4f476c428fade2220/aiohttp-3.12.13-cp311-cp311-win_amd64.whl", hash = "sha256:9a27da9c3b5ed9d04c36ad2df65b38a96a37e9cfba6f1381b842d05d98e6afe9", size = 451427, upload-time = "2025-06-14T15:13:58.505Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491, upload-time = "2025-06-14T15:14:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104, upload-time = "2025-06-14T15:14:01.691Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948, upload-time = "2025-06-14T15:14:03.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742, upload-time = "2025-06-14T15:14:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393, upload-time = "2025-06-14T15:14:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486, upload-time = "2025-06-14T15:14:08.808Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643, upload-time = "2025-06-14T15:14:10.767Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082, upload-time = "2025-06-14T15:14:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884, upload-time = "2025-06-14T15:14:14.415Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943, upload-time = "2025-06-14T15:14:16.48Z" }, + { url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398, upload-time = "2025-06-14T15:14:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051, upload-time = "2025-06-14T15:14:20.223Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611, upload-time = "2025-06-14T15:14:21.988Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586, upload-time = "2025-06-14T15:14:23.979Z" }, + { url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197, upload-time = "2025-06-14T15:14:25.692Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771, upload-time = "2025-06-14T15:14:27.364Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869, upload-time = "2025-06-14T15:14:29.05Z" }, ] [[package]] @@ -217,11 +219,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.4.26" +version = "2025.6.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, ] [[package]] @@ -295,14 +297,14 @@ wheels = [ [[package]] name = "click" -version = "8.2.0" +version = "8.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" }, + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] [[package]] @@ -368,32 +370,34 @@ wheels = [ [[package]] name = "coverage" -version = "7.8.0" +version = "7.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493, upload-time = "2025-03-30T20:35:12.286Z" }, - { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921, upload-time = "2025-03-30T20:35:14.18Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556, upload-time = "2025-03-30T20:35:15.616Z" }, - { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245, upload-time = "2025-03-30T20:35:18.648Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032, upload-time = "2025-03-30T20:35:20.131Z" }, - { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679, upload-time = "2025-03-30T20:35:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852, upload-time = "2025-03-30T20:35:23.525Z" }, - { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389, upload-time = "2025-03-30T20:35:25.09Z" }, - { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997, upload-time = "2025-03-30T20:35:26.914Z" }, - { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911, upload-time = "2025-03-30T20:35:28.498Z" }, - { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684, upload-time = "2025-03-30T20:35:29.959Z" }, - { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935, upload-time = "2025-03-30T20:35:31.912Z" }, - { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994, upload-time = "2025-03-30T20:35:33.455Z" }, - { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885, upload-time = "2025-03-30T20:35:35.354Z" }, - { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142, upload-time = "2025-03-30T20:35:37.121Z" }, - { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906, upload-time = "2025-03-30T20:35:39.07Z" }, - { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124, upload-time = "2025-03-30T20:35:40.598Z" }, - { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317, upload-time = "2025-03-30T20:35:42.204Z" }, - { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170, upload-time = "2025-03-30T20:35:44.216Z" }, - { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969, upload-time = "2025-03-30T20:35:45.797Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443, upload-time = "2025-03-30T20:36:41.959Z" }, - { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" }, + { url = "https://files.pythonhosted.org/packages/60/34/fa69372a07d0903a78ac103422ad34db72281c9fc625eba94ac1185da66f/coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582", size = 212146, upload-time = "2025-06-13T13:00:48.496Z" }, + { url = "https://files.pythonhosted.org/packages/27/f0/da1894915d2767f093f081c42afeba18e760f12fdd7a2f4acbe00564d767/coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86", size = 212536, upload-time = "2025-06-13T13:00:51.535Z" }, + { url = "https://files.pythonhosted.org/packages/10/d5/3fc33b06e41e390f88eef111226a24e4504d216ab8e5d1a7089aa5a3c87a/coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed", size = 245092, upload-time = "2025-06-13T13:00:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/0a/39/7aa901c14977aba637b78e95800edf77f29f5a380d29768c5b66f258305b/coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d", size = 242806, upload-time = "2025-06-13T13:00:54.571Z" }, + { url = "https://files.pythonhosted.org/packages/43/fc/30e5cfeaf560b1fc1989227adedc11019ce4bb7cce59d65db34fe0c2d963/coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338", size = 244610, upload-time = "2025-06-13T13:00:56.932Z" }, + { url = "https://files.pythonhosted.org/packages/bf/15/cca62b13f39650bc87b2b92bb03bce7f0e79dd0bf2c7529e9fc7393e4d60/coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875", size = 244257, upload-time = "2025-06-13T13:00:58.545Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1a/c0f2abe92c29e1464dbd0ff9d56cb6c88ae2b9e21becdb38bea31fcb2f6c/coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250", size = 242309, upload-time = "2025-06-13T13:00:59.836Z" }, + { url = "https://files.pythonhosted.org/packages/57/8d/c6fd70848bd9bf88fa90df2af5636589a8126d2170f3aade21ed53f2b67a/coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c", size = 242898, upload-time = "2025-06-13T13:01:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/6ca46c7bff4675f09a66fe2797cd1ad6a24f14c9c7c3b3ebe0470a6e30b8/coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32", size = 214561, upload-time = "2025-06-13T13:01:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/a1/30/166978c6302010742dabcdc425fa0f938fa5a800908e39aff37a7a876a13/coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125", size = 215493, upload-time = "2025-06-13T13:01:05.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/07/a6d2342cd80a5be9f0eeab115bc5ebb3917b4a64c2953534273cf9bc7ae6/coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e", size = 213869, upload-time = "2025-06-13T13:01:09.345Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" }, + { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" }, + { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" }, + { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" }, + { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" }, + { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e5/c723545c3fd3204ebde3b4cc4b927dce709d3b6dc577754bb57f63ca4a4a/coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", size = 204009, upload-time = "2025-06-13T13:02:25.787Z" }, + { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, ] [package.optional-dependencies] @@ -447,31 +451,31 @@ wheels = [ [[package]] name = "cython" -version = "3.1.1" +version = "3.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/d3/bb000603e46144db2e5055219bbddcf7ab3b10012fcb342695694fb88141/cython-3.1.1.tar.gz", hash = "sha256:505ccd413669d5132a53834d792c707974248088c4f60c497deb1b416e366397", size = 3175446, upload-time = "2025-05-19T09:44:54.347Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/40/7b17cd866158238db704965da1b5849af261dbad393ea3ac966f934b2d39/cython-3.1.2.tar.gz", hash = "sha256:6bbf7a953fa6762dfecdec015e3b054ba51c0121a45ad851fa130f63f5331381", size = 3184825, upload-time = "2025-06-09T07:08:48.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/b3/bc75c0352214b5ced31ce5e0d051d0ad4ad916aa7a1d669d1876ad1e59aa/cython-3.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c360823e1063784efc2335617e0f28573d7a594c5a8a05d85e850a9621cccb1f", size = 2998590, upload-time = "2025-05-19T09:56:51.148Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0a/5840cdd7a1e8c0d2ffeb5e09afd32b8d10321cce33a2554ef10ea832a200/cython-3.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:12e00b88147b03c148a95365f89dc1c45a0fc52f9c35aa75ff770ef65b615839", size = 2860818, upload-time = "2025-05-19T09:56:53.694Z" }, - { url = "https://files.pythonhosted.org/packages/63/2e/0fac02ce46f208af54d76c6c786b8dddeb207ca94aa85b0455f6cbaa472c/cython-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab644415458d782c16ba7252de9cec1e3125371641cafea2e53a8c1cf85dd58d", size = 3124262, upload-time = "2025-05-19T09:56:56.277Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/b7ae247b83b3f98966184c1a787001964132ae2e05a989769b1a5e7325da/cython-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5cb6c054daadaf01a88c8f49f3edd9e829c9b76a82cbb4269e3f9878254540b", size = 3215216, upload-time = "2025-05-19T09:56:58.674Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3b/c4e6c16b099a592dbd6cd615c4de901eb8cc2795d5445d77b8cd378de7da/cython-3.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af8f62cc9339b75fe8434325083e6a7cae88c9c21efd74bbb6ba4e3623219469", size = 3282597, upload-time = "2025-05-19T09:57:00.85Z" }, - { url = "https://files.pythonhosted.org/packages/06/dd/90a8fd8508298f1f16e2cbc665774047fbc81f0370125b6e32f0a182fc10/cython-3.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:689c1aad373556bd2ab1aa1c2dad8939a2891465a1fbd2cbbdd42b488fb40ec8", size = 3175592, upload-time = "2025-05-19T09:57:03.1Z" }, - { url = "https://files.pythonhosted.org/packages/af/ab/2cd4e8d5c46499ea2ca5b7c3ae4053db86465b35a8870ce5e847fee06aff/cython-3.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:953046c190fa9ab9a09a546a909b847cdbb4c1fe34e9bfa4a15b6ee1585a86aa", size = 3378435, upload-time = "2025-05-19T09:57:05.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/99/250fb399af2edd9861774f0c8b6c4b7d862aa52d966a07b860bcd723842a/cython-3.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:755a991601b27dd3555310d0f95b19a05e622a80d7b4e7a91fa6f5f3ef3f3b80", size = 3300519, upload-time = "2025-05-19T09:57:07.512Z" }, - { url = "https://files.pythonhosted.org/packages/37/09/1c5d470580d9b92107cadedc848f43e2f2102284f8b666ea9ab82f6fc101/cython-3.1.1-cp311-cp311-win32.whl", hash = "sha256:83b2af5c327f7da4f08afc34fddfaf6d24fa0c000b6b70a527c8125e493b6080", size = 2447287, upload-time = "2025-05-19T09:57:09.958Z" }, - { url = "https://files.pythonhosted.org/packages/30/67/c99ec81380cd9d2c798eb1572f61dbe50318958925049b39029f73fe6b52/cython-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:141ffd6279411c562f6b707adc56b63e965a4fd7f21db83f5d4fcbd8c50ac546", size = 2655739, upload-time = "2025-05-19T09:57:11.938Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/83ff82381319ff68ae46f9dd3024b1d5101997e81a8e955811525b6f934b/cython-3.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d7dc0e4d0cd491fac679a61e9ede348c64ca449f99a284f9a01851aa1dbc7f6", size = 3006334, upload-time = "2025-05-19T09:57:14.284Z" }, - { url = "https://files.pythonhosted.org/packages/c3/01/b4c46c6a27cd2da642bc987c1f9087265defbc96a1929d326b9034953f15/cython-3.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd689910002adfac8734f237cdea1573e38345f27ed7fd445482813b65a29457", size = 2836861, upload-time = "2025-05-19T09:57:16.129Z" }, - { url = "https://files.pythonhosted.org/packages/96/51/7936c5d01ec3c89be8de1756f284878d4a567627b7b1790455ac627fb833/cython-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10f0434916994fe213ea7749268b88d77e3ebcbd1b99542cf64bb7d180f45470", size = 3074560, upload-time = "2025-05-19T09:57:18.797Z" }, - { url = "https://files.pythonhosted.org/packages/0d/81/34aeb787dcb2624a82a33e60276ed28d2da8a08c79660cf674b19be82248/cython-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:873aac4ac0b0fb197557c0ac15458b780b9221daa4a716881cbd1a9016c8459f", size = 3192645, upload-time = "2025-05-19T09:57:21.002Z" }, - { url = "https://files.pythonhosted.org/packages/e8/bf/1350ed6cb48158a4f096306a12bc4c26c6d20d3314f1f1978ea23afe0220/cython-3.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23b886a6c8a50b1101ccef2f2f3dc9c699b77633ef5bb5007090226c2ad3f9c2", size = 3241751, upload-time = "2025-05-19T09:57:23.118Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f5/9ed5a898c41723e3da2317fd1f082d963ff08571caeded31cb945be589b5/cython-3.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dff0e7dd53a0ca35b64cda843253d5cac944db26663dc097b3a1adf2c49514ad", size = 3123562, upload-time = "2025-05-19T09:57:25.492Z" }, - { url = "https://files.pythonhosted.org/packages/c3/81/b5ce4393d3a0a75a8c6d9ad0b80a62263d892260b816eb3d569ef144511a/cython-3.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f7954b0b4b3302655d3caa6924261de5907a4e129bc22ace52fe9ae0cd5a758", size = 3333555, upload-time = "2025-05-19T09:57:29.232Z" }, - { url = "https://files.pythonhosted.org/packages/db/47/2c1fa4b4901f10d00e666931dd68d4bd7954d3caa900544d135424ef6178/cython-3.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfa500fd7ae95ca152a5f8062b870532fa3e27efcef6d00612e1f28b9f72615f", size = 3282112, upload-time = "2025-05-19T09:57:31.904Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5b/b000d3ebff79429419d846e06c5c09f33fd010e1f6158d7ba553e1082253/cython-3.1.1-cp312-cp312-win32.whl", hash = "sha256:cd748fab8e4426dbcb2e0fa2979558333934d24365e0de5672fbabfe337d880c", size = 2462293, upload-time = "2025-05-19T09:57:34.964Z" }, - { url = "https://files.pythonhosted.org/packages/45/0e/e1370ed3216e4e164232d1891c2a2932a3874d1a8681f8c3565cafd98579/cython-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:307f216ed319ea07644f2ef9974406c830f01bc8e677e2147e9bfcdf9e3ca8ad", size = 2666710, upload-time = "2025-05-19T09:57:37.528Z" }, - { url = "https://files.pythonhosted.org/packages/a7/97/8e8637e67afc09f1b51a617b15a0d1caf0b5159b0f79d47ab101e620e491/cython-3.1.1-py3-none-any.whl", hash = "sha256:07621e044f332d18139df2ccfcc930151fd323c2f61a58c82f304cffc9eb5280", size = 1220898, upload-time = "2025-05-19T09:44:50.614Z" }, + { url = "https://files.pythonhosted.org/packages/1f/de/502ddebaf5fe78f13cd6361acdd74710d3a5b15c22a9edc0ea4c873a59a5/cython-3.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5548573e0912d7dc80579827493315384c462e2f15797b91a8ed177686d31eb9", size = 3007792, upload-time = "2025-06-09T07:09:28.777Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c8/91b00bc68effba9ba1ff5b33988052ac4d98fc1ac3021ade7261661299c6/cython-3.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bf3ea5bc50d80762c490f42846820a868a6406fdb5878ae9e4cc2f11b50228a", size = 2870798, upload-time = "2025-06-09T07:09:30.745Z" }, + { url = "https://files.pythonhosted.org/packages/f4/4b/29d290f14607785112c00a5e1685d766f433531bbd6a11ad229ab61b7a70/cython-3.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20ce53951d06ab2bca39f153d9c5add1d631c2a44d58bf67288c9d631be9724e", size = 3131280, upload-time = "2025-06-09T07:09:32.785Z" }, + { url = "https://files.pythonhosted.org/packages/38/3c/7c61e9ce25377ec7c4aa0b7ceeed34559ebca7b5cfd384672ba64eeaa4ba/cython-3.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e05a36224e3002d48c7c1c695b3771343bd16bc57eab60d6c5d5e08f3cbbafd8", size = 3223898, upload-time = "2025-06-09T07:09:35.345Z" }, + { url = "https://files.pythonhosted.org/packages/10/96/2d3fbe7e50e98b53ac86fefb48b64262b2e1304b3495e8e25b3cd1c3473e/cython-3.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc0fc0777c7ab82297c01c61a1161093a22a41714f62e8c35188a309bd5db8e", size = 3291527, upload-time = "2025-06-09T07:09:37.502Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e4/4cd3624e250d86f05bdb121a567865b9cca75cdc6dce4eedd68e626ea4f8/cython-3.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:18161ef3dd0e90a944daa2be468dd27696712a5f792d6289e97d2a31298ad688", size = 3184034, upload-time = "2025-06-09T07:09:40.225Z" }, + { url = "https://files.pythonhosted.org/packages/24/de/f8c1243c3e50ec95cb81f3a7936c8cf162f28050db8683e291c3861b46a0/cython-3.1.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ca45020950cd52d82189d6dfb6225737586be6fe7b0b9d3fadd7daca62eff531", size = 3386084, upload-time = "2025-06-09T07:09:42.206Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/2365937da44741ef0781bb9ecc1f8f52b38b65acb7293b5fc7c3eaee5346/cython-3.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaae97d6d07610224be2b73a93e9e3dd85c09aedfd8e47054e3ef5a863387dae", size = 3309974, upload-time = "2025-06-09T07:09:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/280eed114110a1a3aa9e2e76bcd06cdd5ef0df7ab77c0be9d5378ca28c57/cython-3.1.2-cp311-cp311-win32.whl", hash = "sha256:3d439d9b19e7e70f6ff745602906d282a853dd5219d8e7abbf355de680c9d120", size = 2482942, upload-time = "2025-06-09T07:09:46.583Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/0aa65be5a4ab65bde3224b8fd23ed795f699d1e724ac109bb0a32036b82d/cython-3.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:8efa44ee2f1876e40eb5e45f6513a19758077c56bf140623ccab43d31f873b61", size = 2686535, upload-time = "2025-06-09T07:09:48.345Z" }, + { url = "https://files.pythonhosted.org/packages/22/86/9393ab7204d5bb65f415dd271b658c18f57b9345d06002cae069376a5a7a/cython-3.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c2c4b6f9a941c857b40168b3f3c81d514e509d985c2dcd12e1a4fea9734192e", size = 3015898, upload-time = "2025-06-09T07:09:50.79Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b8/3d10ac37ab7b7ee60bc6bfb48f6682ebee7fddaccf56e1e135f0d46ca79f/cython-3.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdbc115bbe1b8c1dcbcd1b03748ea87fa967eb8dfc3a1a9bb243d4a382efcff4", size = 2846204, upload-time = "2025-06-09T07:09:52.832Z" }, + { url = "https://files.pythonhosted.org/packages/f8/34/637771d8e10ebabc34a34cdd0d63fe797b66c334e150189955bf6442d710/cython-3.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05111f89db1ca98edc0675cfaa62be47b3ff519a29876eb095532a9f9e052b8", size = 3080671, upload-time = "2025-06-09T07:09:54.924Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c8/383ad1851fb272920a152c5a30bb6f08c3471b5438079d9488fc3074a170/cython-3.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e7188df8709be32cfdfadc7c3782e361c929df9132f95e1bbc90a340dca3c7", size = 3199022, upload-time = "2025-06-09T07:09:56.978Z" }, + { url = "https://files.pythonhosted.org/packages/e6/11/20adc8f2db37a29f245e8fd4b8b8a8245fce4bbbd128185cc9a7b1065e4c/cython-3.1.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c0ecc71e60a051732c2607b8eb8f2a03a5dac09b28e52b8af323c329db9987b", size = 3241337, upload-time = "2025-06-09T07:09:59.156Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0b/491f1fd3e177cccb6bb6d52f9609f78d395edde83ac47ebb06d21717ca29/cython-3.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f27143cf88835c8bcc9bf3304953f23f377d1d991e8942982fe7be344c7cfce3", size = 3131808, upload-time = "2025-06-09T07:10:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/db/d2/5e7053a3214c9baa7ad72940555eb87cf4750e597f10b2bb43db62c3f39f/cython-3.1.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8c43566701133f53bf13485839d8f3f309095fe0d3b9d0cd5873073394d2edc", size = 3340319, upload-time = "2025-06-09T07:10:03.485Z" }, + { url = "https://files.pythonhosted.org/packages/95/42/4842f8ddac9b36c94ae08b23c7fcde3f930c1dd49ac8992bb5320a4d96b5/cython-3.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a3bb893e85f027a929c1764bb14db4c31cbdf8a96f59a78f608f2ba7cfbbce95", size = 3287370, upload-time = "2025-06-09T07:10:05.637Z" }, + { url = "https://files.pythonhosted.org/packages/03/0d/417745ed75d414176e50310087b43299a3e611e75c379ff998f60f2ca1a8/cython-3.1.2-cp312-cp312-win32.whl", hash = "sha256:12c5902f105e43ca9af7874cdf87a23627f98c15d5a4f6d38bc9d334845145c0", size = 2487734, upload-time = "2025-06-09T07:10:07.591Z" }, + { url = "https://files.pythonhosted.org/packages/8e/82/df61d09ab81979ba171a8252af8fb8a3b26a0f19d1330c2679c11fe41667/cython-3.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:06789eb7bd2e55b38b9dd349e9309f794aee0fed99c26ea5c9562d463877763f", size = 2695542, upload-time = "2025-06-09T07:10:09.545Z" }, + { url = "https://files.pythonhosted.org/packages/25/d6/ef8557d5e75cc57d55df579af4976935ee111a85bbee4a5b72354e257066/cython-3.1.2-py3-none-any.whl", hash = "sha256:d23fd7ffd7457205f08571a42b108a3cf993e83a59fe4d72b42e6fc592cf2639", size = 1224753, upload-time = "2025-06-09T07:08:44.849Z" }, ] [[package]] @@ -542,70 +546,70 @@ wheels = [ [[package]] name = "fonttools" -version = "4.58.0" +version = "4.58.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/cf/4d037663e2a1fe30fddb655d755d76e18624be44ad467c07412c2319ab97/fonttools-4.58.0.tar.gz", hash = "sha256:27423d0606a2c7b336913254bf0b1193ebd471d5f725d665e875c5e88a011a43", size = 3514522, upload-time = "2025-05-10T17:36:35.886Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/5a/1124b2c8cb3a8015faf552e92714040bcdbc145dfa29928891b02d147a18/fonttools-4.58.4.tar.gz", hash = "sha256:928a8009b9884ed3aae17724b960987575155ca23c6f0b8146e400cc9e0d44ba", size = 3525026, upload-time = "2025-06-13T17:25:15.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/2e/9b9bd943872a50cb182382f8f4a99af92d76e800603d5f73e4343fdce61a/fonttools-4.58.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9345b1bb994476d6034996b31891c0c728c1059c05daa59f9ab57d2a4dce0f84", size = 2751920, upload-time = "2025-05-10T17:35:16.487Z" }, - { url = "https://files.pythonhosted.org/packages/9b/8c/e8d6375da893125f610826c2e30e6d2597dfb8dad256f8ff5a54f3089fda/fonttools-4.58.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1d93119ace1e2d39ff1340deb71097932f72b21c054bd3da727a3859825e24e5", size = 2313957, upload-time = "2025-05-10T17:35:18.906Z" }, - { url = "https://files.pythonhosted.org/packages/4f/1b/a29cb00c8c20164b24f88780e298fafd0bbfb25cf8bc7b10c4b69331ad5d/fonttools-4.58.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79c9e4f01bb04f19df272ae35314eb6349fdb2e9497a163cd22a21be999694bd", size = 4913808, upload-time = "2025-05-10T17:35:21.394Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ab/9b9507b65b15190cbfe1ccd3c08067d79268d8312ef20948b16d9f5aa905/fonttools-4.58.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62ecda1465d38248aaf9bee1c17a21cf0b16aef7d121d7d303dbb320a6fd49c2", size = 4935876, upload-time = "2025-05-10T17:35:23.849Z" }, - { url = "https://files.pythonhosted.org/packages/15/e4/1395853bc775b0ab06a1c61cf261779afda7baff3f65cf1197bbd21aa149/fonttools-4.58.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29d0499bff12a26733c05c1bfd07e68465158201624b2fba4a40b23d96c43f94", size = 4974798, upload-time = "2025-05-10T17:35:26.189Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b9/0358368ef5462f4653a198207b29885bee8d5e23c870f6125450ed88e693/fonttools-4.58.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1871abdb0af582e2d96cc12d88889e3bfa796928f491ec14d34a2e58ca298c7e", size = 5093560, upload-time = "2025-05-10T17:35:28.577Z" }, - { url = "https://files.pythonhosted.org/packages/11/00/f64bc3659980c41eccf2c371e62eb15b40858f02a41a0e9c6258ef094388/fonttools-4.58.0-cp311-cp311-win32.whl", hash = "sha256:e292485d70402093eb94f6ab7669221743838b8bd4c1f45c84ca76b63338e7bf", size = 2186330, upload-time = "2025-05-10T17:35:31.733Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a0/0287be13a1ec7733abf292ffbd76417cea78752d4ce10fecf92d8b1252d6/fonttools-4.58.0-cp311-cp311-win_amd64.whl", hash = "sha256:6df3755fcf9ad70a74ad3134bd5c9738f73c9bb701a304b1c809877b11fe701c", size = 2234687, upload-time = "2025-05-10T17:35:34.015Z" }, - { url = "https://files.pythonhosted.org/packages/6a/4e/1c6b35ec7c04d739df4cf5aace4b7ec284d6af2533a65de21972e2f237d9/fonttools-4.58.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:aa8316798f982c751d71f0025b372151ea36405733b62d0d94d5e7b8dd674fa6", size = 2737502, upload-time = "2025-05-10T17:35:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/fc/72/c6fcafa3c9ed2b69991ae25a1ba7a3fec8bf74928a96e8229c37faa8eda2/fonttools-4.58.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c6db489511e867633b859b11aefe1b7c0d90281c5bdb903413edbb2ba77b97f1", size = 2307214, upload-time = "2025-05-10T17:35:38.939Z" }, - { url = "https://files.pythonhosted.org/packages/52/11/1015cedc9878da6d8d1758049749eef857b693e5828d477287a959c8650f/fonttools-4.58.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:107bdb2dacb1f627db3c4b77fb16d065a10fe88978d02b4fc327b9ecf8a62060", size = 4811136, upload-time = "2025-05-10T17:35:41.491Z" }, - { url = "https://files.pythonhosted.org/packages/32/b9/6a1bc1af6ec17eead5d32e87075e22d0dab001eace0b5a1542d38c6a9483/fonttools-4.58.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba7212068ab20f1128a0475f169068ba8e5b6e35a39ba1980b9f53f6ac9720ac", size = 4876598, upload-time = "2025-05-10T17:35:43.986Z" }, - { url = "https://files.pythonhosted.org/packages/d8/46/b14584c7ea65ad1609fb9632251016cda8a2cd66b15606753b9f888d3677/fonttools-4.58.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f95ea3b6a3b9962da3c82db73f46d6a6845a6c3f3f968f5293b3ac1864e771c2", size = 4872256, upload-time = "2025-05-10T17:35:46.617Z" }, - { url = "https://files.pythonhosted.org/packages/05/78/b2105a7812ca4ef9bf180cd741c82f4522316c652ce2a56f788e2eb54b62/fonttools-4.58.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:874f1225cc4ccfeac32009887f722d7f8b107ca5e867dcee067597eef9d4c80b", size = 5028710, upload-time = "2025-05-10T17:35:49.227Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a9/a38c85ffd30d1f2c7a5460c8abfd1aa66e00c198df3ff0b08117f5c6fcd9/fonttools-4.58.0-cp312-cp312-win32.whl", hash = "sha256:5f3cde64ec99c43260e2e6c4fa70dfb0a5e2c1c1d27a4f4fe4618c16f6c9ff71", size = 2173593, upload-time = "2025-05-10T17:35:51.226Z" }, - { url = "https://files.pythonhosted.org/packages/66/48/29752962a74b7ed95da976b5a968bba1fe611a4a7e50b9fefa345e6e7025/fonttools-4.58.0-cp312-cp312-win_amd64.whl", hash = "sha256:2aee08e2818de45067109a207cbd1b3072939f77751ef05904d506111df5d824", size = 2223230, upload-time = "2025-05-10T17:35:53.653Z" }, - { url = "https://files.pythonhosted.org/packages/9b/1f/4417c26e26a1feab85a27e927f7a73d8aabc84544be8ba108ce4aa90eb1e/fonttools-4.58.0-py3-none-any.whl", hash = "sha256:c96c36880be2268be409df7b08c5b5dacac1827083461a6bc2cb07b8cbcec1d7", size = 1111440, upload-time = "2025-05-10T17:36:33.607Z" }, + { url = "https://files.pythonhosted.org/packages/17/7b/cc6e9bb41bab223bd2dc70ba0b21386b85f604e27f4c3206b4205085a2ab/fonttools-4.58.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3841991c9ee2dc0562eb7f23d333d34ce81e8e27c903846f0487da21e0028eb", size = 2768901, upload-time = "2025-06-13T17:24:05.901Z" }, + { url = "https://files.pythonhosted.org/packages/3d/15/98d75df9f2b4e7605f3260359ad6e18e027c11fa549f74fce567270ac891/fonttools-4.58.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c98f91b6a9604e7ffb5ece6ea346fa617f967c2c0944228801246ed56084664", size = 2328696, upload-time = "2025-06-13T17:24:09.18Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c8/dc92b80f5452c9c40164e01b3f78f04b835a00e673bd9355ca257008ff61/fonttools-4.58.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab9f891eb687ddf6a4e5f82901e00f992e18012ca97ab7acd15f13632acd14c1", size = 5018830, upload-time = "2025-06-13T17:24:11.282Z" }, + { url = "https://files.pythonhosted.org/packages/19/48/8322cf177680505d6b0b6062e204f01860cb573466a88077a9b795cb70e8/fonttools-4.58.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:891c5771e8f0094b7c0dc90eda8fc75e72930b32581418f2c285a9feedfd9a68", size = 4960922, upload-time = "2025-06-13T17:24:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/14/e0/2aff149ed7eb0916de36da513d473c6fff574a7146891ce42de914899395/fonttools-4.58.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:43ba4d9646045c375d22e3473b7d82b18b31ee2ac715cd94220ffab7bc2d5c1d", size = 4997135, upload-time = "2025-06-13T17:24:16.959Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6f/4d9829b29a64a2e63a121cb11ecb1b6a9524086eef3e35470949837a1692/fonttools-4.58.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33d19f16e6d2ffd6669bda574a6589941f6c99a8d5cfb9f464038244c71555de", size = 5108701, upload-time = "2025-06-13T17:24:18.849Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1e/2d656ddd1b0cd0d222f44b2d008052c2689e66b702b9af1cd8903ddce319/fonttools-4.58.4-cp311-cp311-win32.whl", hash = "sha256:b59e5109b907da19dc9df1287454821a34a75f2632a491dd406e46ff432c2a24", size = 2200177, upload-time = "2025-06-13T17:24:20.823Z" }, + { url = "https://files.pythonhosted.org/packages/fb/83/ba71ad053fddf4157cb0697c8da8eff6718d059f2a22986fa5f312b49c92/fonttools-4.58.4-cp311-cp311-win_amd64.whl", hash = "sha256:3d471a5b567a0d1648f2e148c9a8bcf00d9ac76eb89e976d9976582044cc2509", size = 2247892, upload-time = "2025-06-13T17:24:22.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/3c/1d1792bfe91ef46f22a3d23b4deb514c325e73c17d4f196b385b5e2faf1c/fonttools-4.58.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:462211c0f37a278494e74267a994f6be9a2023d0557aaa9ecbcbfce0f403b5a6", size = 2754082, upload-time = "2025-06-13T17:24:24.862Z" }, + { url = "https://files.pythonhosted.org/packages/2a/1f/2b261689c901a1c3bc57a6690b0b9fc21a9a93a8b0c83aae911d3149f34e/fonttools-4.58.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c7a12fb6f769165547f00fcaa8d0df9517603ae7e04b625e5acb8639809b82d", size = 2321677, upload-time = "2025-06-13T17:24:26.815Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6b/4607add1755a1e6581ae1fc0c9a640648e0d9cdd6591cc2d581c2e07b8c3/fonttools-4.58.4-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d42c63020a922154add0a326388a60a55504629edc3274bc273cd3806b4659f", size = 4896354, upload-time = "2025-06-13T17:24:28.428Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/34b4f483643d0cb11a1f830b72c03fdd18dbd3792d77a2eb2e130a96fada/fonttools-4.58.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f2b4e6fd45edc6805f5f2c355590b092ffc7e10a945bd6a569fc66c1d2ae7aa", size = 4941633, upload-time = "2025-06-13T17:24:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/81/ac/9bafbdb7694059c960de523e643fa5a61dd2f698f3f72c0ca18ae99257c7/fonttools-4.58.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f155b927f6efb1213a79334e4cb9904d1e18973376ffc17a0d7cd43d31981f1e", size = 4886170, upload-time = "2025-06-13T17:24:32.724Z" }, + { url = "https://files.pythonhosted.org/packages/ae/44/a3a3b70d5709405f7525bb7cb497b4e46151e0c02e3c8a0e40e5e9fe030b/fonttools-4.58.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e38f687d5de97c7fb7da3e58169fb5ba349e464e141f83c3c2e2beb91d317816", size = 5037851, upload-time = "2025-06-13T17:24:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/21/cb/e8923d197c78969454eb876a4a55a07b59c9c4c46598f02b02411dc3b45c/fonttools-4.58.4-cp312-cp312-win32.whl", hash = "sha256:636c073b4da9db053aa683db99580cac0f7c213a953b678f69acbca3443c12cc", size = 2187428, upload-time = "2025-06-13T17:24:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/46/e6/fe50183b1a0e1018e7487ee740fa8bb127b9f5075a41e20d017201e8ab14/fonttools-4.58.4-cp312-cp312-win_amd64.whl", hash = "sha256:82e8470535743409b30913ba2822e20077acf9ea70acec40b10fcf5671dceb58", size = 2236649, upload-time = "2025-06-13T17:24:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2f/c536b5b9bb3c071e91d536a4d11f969e911dbb6b227939f4c5b0bca090df/fonttools-4.58.4-py3-none-any.whl", hash = "sha256:a10ce13a13f26cbb9f37512a4346bb437ad7e002ff6fa966a7ce7ff5ac3528bd", size = 1114660, upload-time = "2025-06-13T17:25:13.321Z" }, ] [[package]] name = "frozenlist" -version = "1.6.0" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/f4/d744cba2da59b5c1d88823cf9e8a6c74e4659e2b27604ed973be2a0bf5ab/frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", size = 42831, upload-time = "2025-04-17T22:38:53.099Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/b5/bc883b5296ec902115c00be161da93bf661199c465ec4c483feec6ea4c32/frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d", size = 160912, upload-time = "2025-04-17T22:36:17.235Z" }, - { url = "https://files.pythonhosted.org/packages/6f/93/51b058b563d0704b39c56baa222828043aafcac17fd3734bec5dbeb619b1/frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0", size = 124315, upload-time = "2025-04-17T22:36:18.735Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e0/46cd35219428d350558b874d595e132d1c17a9471a1bd0d01d518a261e7c/frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe", size = 122230, upload-time = "2025-04-17T22:36:20.6Z" }, - { url = "https://files.pythonhosted.org/packages/d1/0f/7ad2ce928ad06d6dd26a61812b959ded573d3e9d0ee6109d96c2be7172e9/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba", size = 314842, upload-time = "2025-04-17T22:36:22.088Z" }, - { url = "https://files.pythonhosted.org/packages/34/76/98cbbd8a20a5c3359a2004ae5e5b216af84a150ccbad67c8f8f30fb2ea91/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595", size = 304919, upload-time = "2025-04-17T22:36:24.247Z" }, - { url = "https://files.pythonhosted.org/packages/9a/fa/258e771ce3a44348c05e6b01dffc2bc67603fba95761458c238cd09a2c77/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a", size = 324074, upload-time = "2025-04-17T22:36:26.291Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a4/047d861fd8c538210e12b208c0479912273f991356b6bdee7ea8356b07c9/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626", size = 321292, upload-time = "2025-04-17T22:36:27.909Z" }, - { url = "https://files.pythonhosted.org/packages/c0/25/cfec8af758b4525676cabd36efcaf7102c1348a776c0d1ad046b8a7cdc65/frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff", size = 301569, upload-time = "2025-04-17T22:36:29.448Z" }, - { url = "https://files.pythonhosted.org/packages/87/2f/0c819372fa9f0c07b153124bf58683b8d0ca7bb73ea5ccde9b9ef1745beb/frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a", size = 313625, upload-time = "2025-04-17T22:36:31.55Z" }, - { url = "https://files.pythonhosted.org/packages/50/5f/f0cf8b0fdedffdb76b3745aa13d5dbe404d63493cc211ce8250f2025307f/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0", size = 312523, upload-time = "2025-04-17T22:36:33.078Z" }, - { url = "https://files.pythonhosted.org/packages/e1/6c/38c49108491272d3e84125bbabf2c2d0b304899b52f49f0539deb26ad18d/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606", size = 322657, upload-time = "2025-04-17T22:36:34.688Z" }, - { url = "https://files.pythonhosted.org/packages/bd/4b/3bd3bad5be06a9d1b04b1c22be80b5fe65b502992d62fab4bdb25d9366ee/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584", size = 303414, upload-time = "2025-04-17T22:36:36.363Z" }, - { url = "https://files.pythonhosted.org/packages/5b/89/7e225a30bef6e85dbfe22622c24afe932e9444de3b40d58b1ea589a14ef8/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a", size = 320321, upload-time = "2025-04-17T22:36:38.16Z" }, - { url = "https://files.pythonhosted.org/packages/22/72/7e3acef4dd9e86366cb8f4d8f28e852c2b7e116927e9722b31a6f71ea4b0/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1", size = 323975, upload-time = "2025-04-17T22:36:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/d8/85/e5da03d20507e13c66ce612c9792b76811b7a43e3320cce42d95b85ac755/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e", size = 316553, upload-time = "2025-04-17T22:36:42.045Z" }, - { url = "https://files.pythonhosted.org/packages/ac/8e/6c609cbd0580ae8a0661c408149f196aade7d325b1ae7adc930501b81acb/frozenlist-1.6.0-cp311-cp311-win32.whl", hash = "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860", size = 115511, upload-time = "2025-04-17T22:36:44.067Z" }, - { url = "https://files.pythonhosted.org/packages/f2/13/a84804cfde6de12d44ed48ecbf777ba62b12ff09e761f76cdd1ff9e14bb1/frozenlist-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603", size = 120863, upload-time = "2025-04-17T22:36:45.465Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8a/289b7d0de2fbac832ea80944d809759976f661557a38bb8e77db5d9f79b7/frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1", size = 160193, upload-time = "2025-04-17T22:36:47.382Z" }, - { url = "https://files.pythonhosted.org/packages/19/80/2fd17d322aec7f430549f0669f599997174f93ee17929ea5b92781ec902c/frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29", size = 123831, upload-time = "2025-04-17T22:36:49.401Z" }, - { url = "https://files.pythonhosted.org/packages/99/06/f5812da431273f78c6543e0b2f7de67dfd65eb0a433978b2c9c63d2205e4/frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25", size = 121862, upload-time = "2025-04-17T22:36:51.899Z" }, - { url = "https://files.pythonhosted.org/packages/d0/31/9e61c6b5fc493cf24d54881731204d27105234d09878be1a5983182cc4a5/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576", size = 316361, upload-time = "2025-04-17T22:36:53.402Z" }, - { url = "https://files.pythonhosted.org/packages/9d/55/22ca9362d4f0222324981470fd50192be200154d51509ee6eb9baa148e96/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8", size = 307115, upload-time = "2025-04-17T22:36:55.016Z" }, - { url = "https://files.pythonhosted.org/packages/ae/39/4fff42920a57794881e7bb3898dc7f5f539261711ea411b43bba3cde8b79/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9", size = 322505, upload-time = "2025-04-17T22:36:57.12Z" }, - { url = "https://files.pythonhosted.org/packages/55/f2/88c41f374c1e4cf0092a5459e5f3d6a1e17ed274c98087a76487783df90c/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e", size = 322666, upload-time = "2025-04-17T22:36:58.735Z" }, - { url = "https://files.pythonhosted.org/packages/75/51/034eeb75afdf3fd03997856195b500722c0b1a50716664cde64e28299c4b/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590", size = 302119, upload-time = "2025-04-17T22:37:00.512Z" }, - { url = "https://files.pythonhosted.org/packages/2b/a6/564ecde55ee633270a793999ef4fd1d2c2b32b5a7eec903b1012cb7c5143/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103", size = 316226, upload-time = "2025-04-17T22:37:02.102Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/6c0682c32377f402b8a6174fb16378b683cf6379ab4d2827c580892ab3c7/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c", size = 312788, upload-time = "2025-04-17T22:37:03.578Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b8/10fbec38f82c5d163ca1750bfff4ede69713badf236a016781cf1f10a0f0/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821", size = 325914, upload-time = "2025-04-17T22:37:05.213Z" }, - { url = "https://files.pythonhosted.org/packages/62/ca/2bf4f3a1bd40cdedd301e6ecfdbb291080d5afc5f9ce350c0739f773d6b9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70", size = 305283, upload-time = "2025-04-17T22:37:06.985Z" }, - { url = "https://files.pythonhosted.org/packages/09/64/20cc13ccf94abc2a1f482f74ad210703dc78a590d0b805af1c9aa67f76f9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f", size = 319264, upload-time = "2025-04-17T22:37:08.618Z" }, - { url = "https://files.pythonhosted.org/packages/20/ff/86c6a2bbe98cfc231519f5e6d712a0898488ceac804a917ce014f32e68f6/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046", size = 326482, upload-time = "2025-04-17T22:37:10.196Z" }, - { url = "https://files.pythonhosted.org/packages/2f/da/8e381f66367d79adca245d1d71527aac774e30e291d41ef161ce2d80c38e/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770", size = 318248, upload-time = "2025-04-17T22:37:12.284Z" }, - { url = "https://files.pythonhosted.org/packages/39/24/1a1976563fb476ab6f0fa9fefaac7616a4361dbe0461324f9fd7bf425dbe/frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc", size = 115161, upload-time = "2025-04-17T22:37:13.902Z" }, - { url = "https://files.pythonhosted.org/packages/80/2e/fb4ed62a65f8cd66044706b1013f0010930d8cbb0729a2219561ea075434/frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878", size = 120548, upload-time = "2025-04-17T22:37:15.326Z" }, - { url = "https://files.pythonhosted.org/packages/71/3e/b04a0adda73bd52b390d730071c0d577073d3d26740ee1bad25c3ad0f37b/frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", size = 12404, upload-time = "2025-04-17T22:38:51.668Z" }, + { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251, upload-time = "2025-06-09T23:00:16.279Z" }, + { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183, upload-time = "2025-06-09T23:00:17.698Z" }, + { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107, upload-time = "2025-06-09T23:00:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333, upload-time = "2025-06-09T23:00:20.275Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724, upload-time = "2025-06-09T23:00:21.705Z" }, + { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842, upload-time = "2025-06-09T23:00:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767, upload-time = "2025-06-09T23:00:25.103Z" }, + { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130, upload-time = "2025-06-09T23:00:27.061Z" }, + { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301, upload-time = "2025-06-09T23:00:29.02Z" }, + { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606, upload-time = "2025-06-09T23:00:30.514Z" }, + { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372, upload-time = "2025-06-09T23:00:31.966Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860, upload-time = "2025-06-09T23:00:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893, upload-time = "2025-06-09T23:00:35.002Z" }, + { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323, upload-time = "2025-06-09T23:00:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149, upload-time = "2025-06-09T23:00:37.963Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565, upload-time = "2025-06-09T23:00:39.753Z" }, + { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019, upload-time = "2025-06-09T23:00:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] [[package]] @@ -810,40 +814,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, ] -[[package]] -name = "lru-dict" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/e3/42c87871920602a3c8300915bd0292f76eccc66c38f782397acbf8a62088/lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b", size = 13123, upload-time = "2023-11-06T01:40:12.951Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/c9/6fac0cb67160f0efa3cc76a6a7d04d5e21a516eeb991ebba08f4f8f01ec5/lru_dict-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:20c595764695d20bdc3ab9b582e0cc99814da183544afb83783a36d6741a0dac", size = 17750, upload-time = "2023-11-06T01:38:52.667Z" }, - { url = "https://files.pythonhosted.org/packages/61/14/f90dee4bc547ae266dbeffd4e11611234bb6af511dea48f3bc8dac1de478/lru_dict-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d9b30a8f50c3fa72a494eca6be5810a1b5c89e4f0fda89374f0d1c5ad8d37d51", size = 11055, upload-time = "2023-11-06T01:38:53.798Z" }, - { url = "https://files.pythonhosted.org/packages/4e/63/a0ae20525f9d52f62ac0def47935f8a2b3b6fcd2c145218b9a27fc1fb910/lru_dict-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9710737584650a4251b9a566cbb1a86f83437adb209c9ba43a4e756d12faf0d7", size = 11330, upload-time = "2023-11-06T01:38:54.847Z" }, - { url = "https://files.pythonhosted.org/packages/e9/c6/8c2b81b61e5206910c81b712500736227289aefe4ccfb36137aa21807003/lru_dict-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b84c321ae34f2f40aae80e18b6fa08b31c90095792ab64bb99d2e385143effaa", size = 31793, upload-time = "2023-11-06T01:38:56.163Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d7/af9733f94df67a2e9e31ef47d4c41aff1836024f135cdbda4743eb628452/lru_dict-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eed24272b4121b7c22f234daed99899817d81d671b3ed030c876ac88bc9dc890", size = 33090, upload-time = "2023-11-06T01:38:57.091Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6e/5b09b069a70028bcf05dbdc57a301fbe8b3bafecf916f2ed5a3065c79a71/lru_dict-1.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd13af06dab7c6ee92284fd02ed9a5613a07d5c1b41948dc8886e7207f86dfd", size = 29795, upload-time = "2023-11-06T01:38:58.278Z" }, - { url = "https://files.pythonhosted.org/packages/21/92/4690daefc2602f7c3429ecf54572d37a9e3c372d370344d2185daa4d5ecc/lru_dict-1.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1efc59bfba6aac33684d87b9e02813b0e2445b2f1c444dae2a0b396ad0ed60c", size = 31586, upload-time = "2023-11-06T01:38:59.363Z" }, - { url = "https://files.pythonhosted.org/packages/3c/67/0a29a91087196b02f278d8765120ee4e7486f1f72a4c505fd1cd3109e627/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cfaf75ac574447afcf8ad998789071af11d2bcf6f947643231f692948839bd98", size = 36662, upload-time = "2023-11-06T01:39:00.795Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/8d56c514cd2333b652bd44c8f1962ab986cbe68e8ad7258c9e0f360cddb6/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c95f8751e2abd6f778da0399c8e0239321d560dbc58cb063827123137d213242", size = 35118, upload-time = "2023-11-06T01:39:01.883Z" }, - { url = "https://files.pythonhosted.org/packages/f5/9a/c7a175d10d503b86974cb07141ca175947145dd1c7370fcda86fbbcaf326/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:abd0c284b26b5c4ee806ca4f33ab5e16b4bf4d5ec9e093e75a6f6287acdde78e", size = 38198, upload-time = "2023-11-06T01:39:03.306Z" }, - { url = "https://files.pythonhosted.org/packages/fd/59/2e5086c8e8a05a7282a824a2a37e3c45cd5714e7b83d8bc0267cb3bb5b4f/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a47740652b25900ac5ce52667b2eade28d8b5fdca0ccd3323459df710e8210a", size = 36542, upload-time = "2023-11-06T01:39:04.751Z" }, - { url = "https://files.pythonhosted.org/packages/12/52/80d0a06e5f45fe7c278dd662da6ea5b39f2ff003248f448189932f6b71c2/lru_dict-1.3.0-cp311-cp311-win32.whl", hash = "sha256:a690c23fc353681ed8042d9fe8f48f0fb79a57b9a45daea2f0be1eef8a1a4aa4", size = 12533, upload-time = "2023-11-06T01:39:05.838Z" }, - { url = "https://files.pythonhosted.org/packages/ce/fe/1f12f33513310860ec6d722709ec4ad8256d9dcc3385f6ae2a244e6e66f5/lru_dict-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:efd3f4e0385d18f20f7ea6b08af2574c1bfaa5cb590102ef1bee781bdfba84bc", size = 13651, upload-time = "2023-11-06T01:39:06.871Z" }, - { url = "https://files.pythonhosted.org/packages/fc/5c/385f080747eb3083af87d8e4c9068f3c4cab89035f6982134889940dafd8/lru_dict-1.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c279068f68af3b46a5d649855e1fb87f5705fe1f744a529d82b2885c0e1fc69d", size = 17174, upload-time = "2023-11-06T01:39:07.923Z" }, - { url = "https://files.pythonhosted.org/packages/3c/de/5ef2ed75ce55d7059d1b96177ba04fa7ee1f35564f97bdfcd28fccfbe9d2/lru_dict-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:350e2233cfee9f326a0d7a08e309372d87186565e43a691b120006285a0ac549", size = 10742, upload-time = "2023-11-06T01:39:08.871Z" }, - { url = "https://files.pythonhosted.org/packages/ca/05/f69a6abb0062d2cf2ce0aaf0284b105b97d1da024ca6d3d0730e6151242e/lru_dict-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4eafb188a84483b3231259bf19030859f070321b00326dcb8e8c6cbf7db4b12f", size = 11079, upload-time = "2023-11-06T01:39:09.766Z" }, - { url = "https://files.pythonhosted.org/packages/ea/59/cf891143abe58a455b8eaa9175f0e80f624a146a2bf9a1ca842ee0ef930a/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73593791047e36b37fdc0b67b76aeed439fcea80959c7d46201240f9ec3b2563", size = 32469, upload-time = "2023-11-06T01:39:11.091Z" }, - { url = "https://files.pythonhosted.org/packages/59/88/d5976e9f70107ce11e45d93c6f0c2d5eaa1fc30bb3c8f57525eda4510dff/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1958cb70b9542773d6241974646e5410e41ef32e5c9e437d44040d59bd80daf2", size = 33496, upload-time = "2023-11-06T01:39:12.463Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f8/94d6e910d54fc1fa05c0ee1cd608c39401866a18cf5e5aff238449b33c11/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc1cd3ed2cee78a47f11f3b70be053903bda197a873fd146e25c60c8e5a32cd6", size = 29914, upload-time = "2023-11-06T01:39:13.395Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b9/9db79780c8a3cfd66bba6847773061e5cf8a3746950273b9985d47bbfe53/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eb230d48eaebd6977a92ddaa6d788f14cf4f4bcf5bbffa4ddfd60d051aa9d4", size = 32241, upload-time = "2023-11-06T01:39:14.612Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b6/08a623019daec22a40c4d6d2c40851dfa3d129a53b2f9469db8eb13666c1/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5ad659cbc349d0c9ba8e536b5f40f96a70c360f43323c29f4257f340d891531c", size = 37320, upload-time = "2023-11-06T01:39:15.875Z" }, - { url = "https://files.pythonhosted.org/packages/70/0b/d3717159c26155ff77679cee1b077d22e1008bf45f19921e193319cd8e46/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba490b8972531d153ac0d4e421f60d793d71a2f4adbe2f7740b3c55dce0a12f1", size = 35054, upload-time = "2023-11-06T01:39:17.063Z" }, - { url = "https://files.pythonhosted.org/packages/04/74/f2ae00de7c27984a19b88d2b09ac877031c525b01199d7841ec8fa657fd6/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:c0131351b8a7226c69f1eba5814cbc9d1d8daaf0fdec1ae3f30508e3de5262d4", size = 38613, upload-time = "2023-11-06T01:39:18.136Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0b/e30236aafe31b4247aa9ae61ba8aac6dde75c3ea0e47a8fb7eef53f6d5ce/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0e88dba16695f17f41701269fa046197a3fd7b34a8dba744c8749303ddaa18df", size = 37143, upload-time = "2023-11-06T01:39:19.571Z" }, - { url = "https://files.pythonhosted.org/packages/1c/28/b59bcebb8d76ba8147a784a8be7eab6a4ad3395b9236e73740ff675a5a52/lru_dict-1.3.0-cp312-cp312-win32.whl", hash = "sha256:6ffaf595e625b388babc8e7d79b40f26c7485f61f16efe76764e32dce9ea17fc", size = 12653, upload-time = "2023-11-06T01:39:20.574Z" }, - { url = "https://files.pythonhosted.org/packages/bd/18/06d9710cb0a0d3634f8501e4bdcc07abe64a32e404d82895a6a36fab97f6/lru_dict-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf9da32ef2582434842ab6ba6e67290debfae72771255a8e8ab16f3e006de0aa", size = 13811, upload-time = "2023-11-06T01:39:21.599Z" }, -] - [[package]] name = "lxml" version = "5.4.0" @@ -888,11 +858,11 @@ wheels = [ [[package]] name = "markdown" -version = "3.8" +version = "3.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906, upload-time = "2025-04-11T14:42:50.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210, upload-time = "2025-04-11T14:42:49.178Z" }, + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, ] [[package]] @@ -1103,70 +1073,73 @@ wheels = [ [[package]] name = "multidict" -version = "6.4.4" +version = "6.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/2f/a3470242707058fe856fe59241eee5635d79087100b7042a867368863a27/multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8", size = 90183, upload-time = "2025-05-19T14:16:37.381Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/b5/59f27b4ce9951a4bce56b88ba5ff5159486797ab18863f2b4c1c5e8465bd/multidict-6.5.0.tar.gz", hash = "sha256:942bd8002492ba819426a8d7aefde3189c1b87099cdf18aaaefefcf7f3f7b6d2", size = 98512, upload-time = "2025-06-17T14:15:56.556Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/1b/4c6e638195851524a63972c5773c7737bea7e47b1ba402186a37773acee2/multidict-6.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95", size = 65515, upload-time = "2025-05-19T14:14:19.767Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/10e6bca9a44b8af3c7f920743e5fc0c2bcf8c11bf7a295d4cfe00b08fb46/multidict-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a", size = 38609, upload-time = "2025-05-19T14:14:21.538Z" }, - { url = "https://files.pythonhosted.org/packages/26/b4/91fead447ccff56247edc7f0535fbf140733ae25187a33621771ee598a18/multidict-6.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223", size = 37871, upload-time = "2025-05-19T14:14:22.666Z" }, - { url = "https://files.pythonhosted.org/packages/3b/37/cbc977cae59277e99d15bbda84cc53b5e0c4929ffd91d958347200a42ad0/multidict-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44", size = 226661, upload-time = "2025-05-19T14:14:24.124Z" }, - { url = "https://files.pythonhosted.org/packages/15/cd/7e0b57fbd4dc2fc105169c4ecce5be1a63970f23bb4ec8c721b67e11953d/multidict-6.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065", size = 223422, upload-time = "2025-05-19T14:14:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/f1/01/1de268da121bac9f93242e30cd3286f6a819e5f0b8896511162d6ed4bf8d/multidict-6.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f", size = 235447, upload-time = "2025-05-19T14:14:26.793Z" }, - { url = "https://files.pythonhosted.org/packages/d2/8c/8b9a5e4aaaf4f2de14e86181a3a3d7b105077f668b6a06f043ec794f684c/multidict-6.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a", size = 231455, upload-time = "2025-05-19T14:14:28.149Z" }, - { url = "https://files.pythonhosted.org/packages/35/db/e1817dcbaa10b319c412769cf999b1016890849245d38905b73e9c286862/multidict-6.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2", size = 223666, upload-time = "2025-05-19T14:14:29.584Z" }, - { url = "https://files.pythonhosted.org/packages/4a/e1/66e8579290ade8a00e0126b3d9a93029033ffd84f0e697d457ed1814d0fc/multidict-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1", size = 217392, upload-time = "2025-05-19T14:14:30.961Z" }, - { url = "https://files.pythonhosted.org/packages/7b/6f/f8639326069c24a48c7747c2a5485d37847e142a3f741ff3340c88060a9a/multidict-6.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42", size = 228969, upload-time = "2025-05-19T14:14:32.672Z" }, - { url = "https://files.pythonhosted.org/packages/d2/c3/3d58182f76b960eeade51c89fcdce450f93379340457a328e132e2f8f9ed/multidict-6.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e", size = 217433, upload-time = "2025-05-19T14:14:34.016Z" }, - { url = "https://files.pythonhosted.org/packages/e1/4b/f31a562906f3bd375f3d0e83ce314e4a660c01b16c2923e8229b53fba5d7/multidict-6.4.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd", size = 225418, upload-time = "2025-05-19T14:14:35.376Z" }, - { url = "https://files.pythonhosted.org/packages/99/89/78bb95c89c496d64b5798434a3deee21996114d4d2c28dd65850bf3a691e/multidict-6.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925", size = 235042, upload-time = "2025-05-19T14:14:36.723Z" }, - { url = "https://files.pythonhosted.org/packages/74/91/8780a6e5885a8770442a8f80db86a0887c4becca0e5a2282ba2cae702bc4/multidict-6.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c", size = 230280, upload-time = "2025-05-19T14:14:38.194Z" }, - { url = "https://files.pythonhosted.org/packages/68/c1/fcf69cabd542eb6f4b892469e033567ee6991d361d77abdc55e3a0f48349/multidict-6.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08", size = 223322, upload-time = "2025-05-19T14:14:40.015Z" }, - { url = "https://files.pythonhosted.org/packages/b8/85/5b80bf4b83d8141bd763e1d99142a9cdfd0db83f0739b4797172a4508014/multidict-6.4.4-cp311-cp311-win32.whl", hash = "sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49", size = 35070, upload-time = "2025-05-19T14:14:41.904Z" }, - { url = "https://files.pythonhosted.org/packages/09/66/0bed198ffd590ab86e001f7fa46b740d58cf8ff98c2f254e4a36bf8861ad/multidict-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529", size = 38667, upload-time = "2025-05-19T14:14:43.534Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b5/5675377da23d60875fe7dae6be841787755878e315e2f517235f22f59e18/multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2", size = 64293, upload-time = "2025-05-19T14:14:44.724Z" }, - { url = "https://files.pythonhosted.org/packages/34/a7/be384a482754bb8c95d2bbe91717bf7ccce6dc38c18569997a11f95aa554/multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d", size = 38096, upload-time = "2025-05-19T14:14:45.95Z" }, - { url = "https://files.pythonhosted.org/packages/66/6d/d59854bb4352306145bdfd1704d210731c1bb2c890bfee31fb7bbc1c4c7f/multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a", size = 37214, upload-time = "2025-05-19T14:14:47.158Z" }, - { url = "https://files.pythonhosted.org/packages/99/e0/c29d9d462d7cfc5fc8f9bf24f9c6843b40e953c0b55e04eba2ad2cf54fba/multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f", size = 224686, upload-time = "2025-05-19T14:14:48.366Z" }, - { url = "https://files.pythonhosted.org/packages/dc/4a/da99398d7fd8210d9de068f9a1b5f96dfaf67d51e3f2521f17cba4ee1012/multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93", size = 231061, upload-time = "2025-05-19T14:14:49.952Z" }, - { url = "https://files.pythonhosted.org/packages/21/f5/ac11add39a0f447ac89353e6ca46666847051103649831c08a2800a14455/multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780", size = 232412, upload-time = "2025-05-19T14:14:51.812Z" }, - { url = "https://files.pythonhosted.org/packages/d9/11/4b551e2110cded705a3c13a1d4b6a11f73891eb5a1c449f1b2b6259e58a6/multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482", size = 231563, upload-time = "2025-05-19T14:14:53.262Z" }, - { url = "https://files.pythonhosted.org/packages/4c/02/751530c19e78fe73b24c3da66618eda0aa0d7f6e7aa512e46483de6be210/multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1", size = 223811, upload-time = "2025-05-19T14:14:55.232Z" }, - { url = "https://files.pythonhosted.org/packages/c7/cb/2be8a214643056289e51ca356026c7b2ce7225373e7a1f8c8715efee8988/multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275", size = 216524, upload-time = "2025-05-19T14:14:57.226Z" }, - { url = "https://files.pythonhosted.org/packages/19/f3/6d5011ec375c09081f5250af58de85f172bfcaafebff286d8089243c4bd4/multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b", size = 229012, upload-time = "2025-05-19T14:14:58.597Z" }, - { url = "https://files.pythonhosted.org/packages/67/9c/ca510785df5cf0eaf5b2a8132d7d04c1ce058dcf2c16233e596ce37a7f8e/multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2", size = 226765, upload-time = "2025-05-19T14:15:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/36/c8/ca86019994e92a0f11e642bda31265854e6ea7b235642f0477e8c2e25c1f/multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc", size = 222888, upload-time = "2025-05-19T14:15:01.568Z" }, - { url = "https://files.pythonhosted.org/packages/c6/67/bc25a8e8bd522935379066950ec4e2277f9b236162a73548a2576d4b9587/multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed", size = 234041, upload-time = "2025-05-19T14:15:03.759Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a0/70c4c2d12857fccbe607b334b7ee28b6b5326c322ca8f73ee54e70d76484/multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740", size = 231046, upload-time = "2025-05-19T14:15:05.698Z" }, - { url = "https://files.pythonhosted.org/packages/c1/0f/52954601d02d39742aab01d6b92f53c1dd38b2392248154c50797b4df7f1/multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e", size = 227106, upload-time = "2025-05-19T14:15:07.124Z" }, - { url = "https://files.pythonhosted.org/packages/af/24/679d83ec4379402d28721790dce818e5d6b9f94ce1323a556fb17fa9996c/multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b", size = 35351, upload-time = "2025-05-19T14:15:08.556Z" }, - { url = "https://files.pythonhosted.org/packages/52/ef/40d98bc5f986f61565f9b345f102409534e29da86a6454eb6b7c00225a13/multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781", size = 38791, upload-time = "2025-05-19T14:15:09.825Z" }, - { url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481, upload-time = "2025-05-19T14:16:36.024Z" }, + { url = "https://files.pythonhosted.org/packages/75/ba/484f8e96ee58ec4fef42650eb9dbbedb24f9bc155780888398a4725d2270/multidict-6.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8b4bf6bb15a05796a07a248084e3e46e032860c899c7a9b981030e61368dba95", size = 73283, upload-time = "2025-06-17T14:13:50.406Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/01d62ea6199d76934c87746695b3ed16aeedfdd564e8d89184577037baac/multidict-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46bb05d50219655c42a4b8fcda9c7ee658a09adbb719c48e65a20284e36328ea", size = 42937, upload-time = "2025-06-17T14:13:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/da/cf/bb462d920f26d9e2e0aff8a78aeb06af1225b826e9a5468870c57591910a/multidict-6.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:54f524d73f4d54e87e03c98f6af601af4777e4668a52b1bd2ae0a4d6fc7b392b", size = 42748, upload-time = "2025-06-17T14:13:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b1/d5c11ea0fdad68d3ed45f0e2527de6496d2fac8afe6b8ca6d407c20ad00f/multidict-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529b03600466480ecc502000d62e54f185a884ed4570dee90d9a273ee80e37b5", size = 236448, upload-time = "2025-06-17T14:13:53.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/69/c3ceb264994f5b338c812911a8d660084f37779daef298fc30bd817f75c7/multidict-6.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69ad681ad7c93a41ee7005cc83a144b5b34a3838bcf7261e2b5356057b0f78de", size = 228695, upload-time = "2025-06-17T14:13:54.775Z" }, + { url = "https://files.pythonhosted.org/packages/81/3d/c23dcc0d34a35ad29974184db2878021d28fe170ecb9192be6bfee73f1f2/multidict-6.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fe9fada8bc0839466b09fa3f6894f003137942984843ec0c3848846329a36ae", size = 247434, upload-time = "2025-06-17T14:13:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/06/b3/06cf7a049129ff52525a859277abb5648e61d7afae7fb7ed02e3806be34e/multidict-6.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f94c6ea6405fcf81baef1e459b209a78cda5442e61b5b7a57ede39d99b5204a0", size = 239431, upload-time = "2025-06-17T14:13:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/8a/72/b2fe2fafa23af0c6123aebe23b4cd23fdad01dfe7009bb85624e4636d0dd/multidict-6.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ca75ad8a39ed75f079a8931435a5b51ee4c45d9b32e1740f99969a5d1cc2ee", size = 231542, upload-time = "2025-06-17T14:13:58.597Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c9/a52ca0a342a02411a31b6af197a6428a5137d805293f10946eeab614ec06/multidict-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4c08f3a2a6cc42b414496017928d95898964fed84b1b2dace0c9ee763061f9", size = 233069, upload-time = "2025-06-17T14:13:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/a3328a3929b8e131e2678d5e65f552b0a6874fab62123e31f5a5625650b0/multidict-6.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:046a7540cfbb4d5dc846a1fd9843f3ba980c6523f2e0c5b8622b4a5c94138ae6", size = 250596, upload-time = "2025-06-17T14:14:01.178Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b8/aa3905a38a8287013aeb0a54c73f79ccd8b32d2f1d53e5934643a36502c2/multidict-6.5.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64306121171d988af77d74be0d8c73ee1a69cf6f96aea7fa6030c88f32a152dd", size = 237858, upload-time = "2025-06-17T14:14:03.232Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/f11d5af028014f402e5dd01ece74533964fa4e7bfae4af4824506fa8c398/multidict-6.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b4ac1dd5eb0ecf6f7351d5a9137f30a83f7182209c5d37f61614dfdce5714853", size = 249175, upload-time = "2025-06-17T14:14:04.561Z" }, + { url = "https://files.pythonhosted.org/packages/ac/57/d451905a62e5ef489cb4f92e8190d34ac5329427512afd7f893121da4e96/multidict-6.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bab4a8337235365f4111a7011a1f028826ca683834ebd12de4b85e2844359c36", size = 259532, upload-time = "2025-06-17T14:14:05.798Z" }, + { url = "https://files.pythonhosted.org/packages/d3/90/ff82b5ac5cabe3c79c50cf62a62f3837905aa717e67b6b4b7872804f23c8/multidict-6.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a05b5604c5a75df14a63eeeca598d11b2c3745b9008539b70826ea044063a572", size = 250554, upload-time = "2025-06-17T14:14:07.382Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5a/0cabc50d4bc16e61d8b0a8a74499a1409fa7b4ef32970b7662a423781fc7/multidict-6.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67c4a640952371c9ca65b6a710598be246ef3be5ca83ed38c16a7660d3980877", size = 248159, upload-time = "2025-06-17T14:14:08.65Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1d/adeabae0771544f140d9f42ab2c46eaf54e793325999c36106078b7f6600/multidict-6.5.0-cp311-cp311-win32.whl", hash = "sha256:fdeae096ca36c12d8aca2640b8407a9d94e961372c68435bef14e31cce726138", size = 40357, upload-time = "2025-06-17T14:14:09.91Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/bbd85ae65c96de5c9910c332ee1f4b7be0bf0fb21563895167bcb6502a1f/multidict-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:e2977ef8b7ce27723ee8c610d1bd1765da4f3fbe5a64f9bf1fd3b4770e31fbc0", size = 44432, upload-time = "2025-06-17T14:14:11.013Z" }, + { url = "https://files.pythonhosted.org/packages/96/af/f9052d9c4e65195b210da9f7afdea06d3b7592b3221cc0ef1b407f762faa/multidict-6.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:82d0cf0ea49bae43d9e8c3851e21954eff716259ff42da401b668744d1760bcb", size = 41408, upload-time = "2025-06-17T14:14:12.112Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fa/18f4950e00924f7e84c8195f4fc303295e14df23f713d64e778b8fa8b903/multidict-6.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1bb986c8ea9d49947bc325c51eced1ada6d8d9b4c5b15fd3fcdc3c93edef5a74", size = 73474, upload-time = "2025-06-17T14:14:13.528Z" }, + { url = "https://files.pythonhosted.org/packages/6c/66/0392a2a8948bccff57e4793c9dde3e5c088f01e8b7f8867ee58a2f187fc5/multidict-6.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:03c0923da300120830fc467e23805d63bbb4e98b94032bd863bc7797ea5fa653", size = 43741, upload-time = "2025-06-17T14:14:15.188Z" }, + { url = "https://files.pythonhosted.org/packages/98/3e/f48487c91b2a070566cfbab876d7e1ebe7deb0a8002e4e896a97998ae066/multidict-6.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4c78d5ec00fdd35c91680ab5cf58368faad4bd1a8721f87127326270248de9bc", size = 42143, upload-time = "2025-06-17T14:14:16.612Z" }, + { url = "https://files.pythonhosted.org/packages/3f/49/439c6cc1cd00365cf561bdd3579cc3fa1a0d38effb3a59b8d9562839197f/multidict-6.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadc3cb78be90a887f8f6b73945b840da44b4a483d1c9750459ae69687940c97", size = 239303, upload-time = "2025-06-17T14:14:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/c4/24/491786269e90081cb536e4d7429508725bc92ece176d1204a4449de7c41c/multidict-6.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b02e1ca495d71e07e652e4cef91adae3bf7ae4493507a263f56e617de65dafc", size = 236913, upload-time = "2025-06-17T14:14:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/bbe2558b820ebeca8a317ab034541790e8160ca4b1e450415383ac69b339/multidict-6.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fe92a62326eef351668eec4e2dfc494927764a0840a1895cff16707fceffcd3", size = 250752, upload-time = "2025-06-17T14:14:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e3/3977f2c1123f553ceff9f53cd4de04be2c1912333c6fabbcd51531655476/multidict-6.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7673ee4f63879ecd526488deb1989041abcb101b2d30a9165e1e90c489f3f7fb", size = 243937, upload-time = "2025-06-17T14:14:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b8/7a6e9c13c79709cdd2f22ee849f058e6da76892d141a67acc0e6c30d845c/multidict-6.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa097ae2a29f573de7e2d86620cbdda5676d27772d4ed2669cfa9961a0d73955", size = 237419, upload-time = "2025-06-17T14:14:23.215Z" }, + { url = "https://files.pythonhosted.org/packages/84/9d/8557f5e88da71bc7e7a8ace1ada4c28197f3bfdc2dd6e51d3b88f2e16e8e/multidict-6.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:300da0fa4f8457d9c4bd579695496116563409e676ac79b5e4dca18e49d1c308", size = 237222, upload-time = "2025-06-17T14:14:24.516Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3b/8f023ad60e7969cb6bc0683738d0e1618f5ff5723d6d2d7818dc6df6ad3d/multidict-6.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a19bd108c35877b57393243d392d024cfbfdefe759fd137abb98f6fc910b64c", size = 247861, upload-time = "2025-06-17T14:14:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/af/1c/9cf5a099ce7e3189906cf5daa72c44ee962dcb4c1983659f3a6f8a7446ab/multidict-6.5.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f32a1777465a35c35ddbbd7fc1293077938a69402fcc59e40b2846d04a120dd", size = 243917, upload-time = "2025-06-17T14:14:27.164Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bb/88ee66ebeef56868044bac58feb1cc25658bff27b20e3cfc464edc181287/multidict-6.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9cc1e10c14ce8112d1e6d8971fe3cdbe13e314f68bea0e727429249d4a6ce164", size = 249214, upload-time = "2025-06-17T14:14:28.795Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/a90e88cc4a1309f33088ab1cdd5c0487718f49dfb82c5ffc845bb17c1973/multidict-6.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e95c5e07a06594bdc288117ca90e89156aee8cb2d7c330b920d9c3dd19c05414", size = 258682, upload-time = "2025-06-17T14:14:30.066Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/16dd69a6811920a31f4e06114ebe67b1cd922c8b05c9c82b050706d0b6fe/multidict-6.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40ff26f58323795f5cd2855e2718a1720a1123fb90df4553426f0efd76135462", size = 254254, upload-time = "2025-06-17T14:14:31.323Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a8/90193a5f5ca1bdbf92633d69a25a2ef9bcac7b412b8d48c84d01a2732518/multidict-6.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76803a29fd71869a8b59c2118c9dcfb3b8f9c8723e2cce6baeb20705459505cf", size = 247741, upload-time = "2025-06-17T14:14:32.717Z" }, + { url = "https://files.pythonhosted.org/packages/cd/43/29c7a747153c05b41d1f67455426af39ed88d6de3f21c232b8f2724bde13/multidict-6.5.0-cp312-cp312-win32.whl", hash = "sha256:df7ecbc65a53a2ce1b3a0c82e6ad1a43dcfe7c6137733f9176a92516b9f5b851", size = 41049, upload-time = "2025-06-17T14:14:33.941Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e8/8f3fc32b7e901f3a2719764d64aeaf6ae77b4ba961f1c3a3cf3867766636/multidict-6.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ec1c3fbbb0b655a6540bce408f48b9a7474fd94ed657dcd2e890671fefa7743", size = 44700, upload-time = "2025-06-17T14:14:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/24/e4/e250806adc98d524d41e69c8d4a42bc3513464adb88cb96224df12928617/multidict-6.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:2d24a00d34808b22c1f15902899b9d82d0faeca9f56281641c791d8605eacd35", size = 41703, upload-time = "2025-06-17T14:14:36.168Z" }, + { url = "https://files.pythonhosted.org/packages/44/d8/45e8fc9892a7386d074941429e033adb4640e59ff0780d96a8cf46fe788e/multidict-6.5.0-py3-none-any.whl", hash = "sha256:5634b35f225977605385f56153bd95a7133faffc0ffe12ad26e10517537e8dfc", size = 12181, upload-time = "2025-06-17T14:15:55.156Z" }, ] [[package]] name = "mypy" -version = "1.15.0" +version = "1.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, + { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload-time = "2025-02-05T03:50:17.287Z" }, - { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload-time = "2025-02-05T03:49:51.21Z" }, - { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload-time = "2025-02-05T03:50:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload-time = "2025-02-05T03:49:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload-time = "2025-02-05T03:49:07.707Z" }, - { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload-time = "2025-02-05T03:49:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981, upload-time = "2025-02-05T03:50:28.25Z" }, - { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175, upload-time = "2025-02-05T03:50:13.411Z" }, - { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675, upload-time = "2025-02-05T03:50:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020, upload-time = "2025-02-05T03:48:48.705Z" }, - { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582, upload-time = "2025-02-05T03:49:03.628Z" }, - { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614, upload-time = "2025-02-05T03:50:00.313Z" }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" }, + { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" }, + { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" }, + { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" }, + { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, ] [[package]] @@ -1283,6 +1256,7 @@ dependencies = [ { name = "pyopenssl" }, { name = "pyserial" }, { name = "pyzmq" }, + { name = "qrcode" }, { name = "requests" }, { name = "scons" }, { name = "sentry-sdk" }, @@ -1305,8 +1279,8 @@ dev = [ { name = "azure-storage-blob" }, { name = "dbus-next" }, { name = "dictdiffer" }, - { name = "lru-dict" }, { name = "matplotlib" }, + { name = "opencv-python-headless" }, { name = "parameterized" }, { name = "pyautogui" }, { name = "pygame" }, @@ -1369,7 +1343,6 @@ requires-dist = [ { name = "json-rpc" }, { name = "libusb1" }, { name = "llvmlite" }, - { name = "lru-dict", marker = "extra == 'dev'" }, { name = "matplotlib", marker = "extra == 'dev'" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" }, { name = "mkdocs", marker = "extra == 'docs'" }, @@ -1377,6 +1350,7 @@ requires-dist = [ { name = "natsort", marker = "extra == 'docs'" }, { name = "numpy", specifier = ">=2.0,<2.2" }, { name = "onnx", specifier = ">=1.14.0" }, + { name = "opencv-python-headless", marker = "extra == 'dev'" }, { name = "parameterized", marker = "extra == 'dev'", specifier = ">=0.8,<0.9" }, { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "psutil" }, @@ -1399,10 +1373,11 @@ requires-dist = [ { name = "pytest-repeat", marker = "extra == 'testing'" }, { name = "pytest-subtests", marker = "extra == 'testing'" }, { name = "pytest-timeout", marker = "extra == 'testing'" }, - { name = "pytest-xdist", marker = "extra == 'testing'" }, + { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=909e97b49d12401c10608f9d777bfc9dab8a4413" }, { name = "pytools", marker = "platform_machine != 'aarch64' and extra == 'dev'", specifier = "<2024.1.11" }, { name = "pywinctl", marker = "extra == 'dev'" }, { name = "pyzmq" }, + { name = "qrcode" }, { name = "raylib", marker = "extra == 'dev'" }, { name = "requests" }, { name = "rerun-sdk", marker = "extra == 'tools'", specifier = ">=0.18" }, @@ -1571,57 +1546,57 @@ sdist = { url = "https://files.pythonhosted.org/packages/a3/a6/b8e451f6cff1c99b4 [[package]] name = "propcache" -version = "0.3.1" +version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651, upload-time = "2025-03-26T03:06:12.05Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/0f/5a5319ee83bd651f75311fcb0c492c21322a7fc8f788e4eef23f44243427/propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5", size = 80243, upload-time = "2025-03-26T03:04:01.912Z" }, - { url = "https://files.pythonhosted.org/packages/ce/84/3db5537e0879942783e2256616ff15d870a11d7ac26541336fe1b673c818/propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371", size = 46503, upload-time = "2025-03-26T03:04:03.704Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c8/b649ed972433c3f0d827d7f0cf9ea47162f4ef8f4fe98c5f3641a0bc63ff/propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da", size = 45934, upload-time = "2025-03-26T03:04:05.257Z" }, - { url = "https://files.pythonhosted.org/packages/59/f9/4c0a5cf6974c2c43b1a6810c40d889769cc8f84cea676cbe1e62766a45f8/propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744", size = 233633, upload-time = "2025-03-26T03:04:07.044Z" }, - { url = "https://files.pythonhosted.org/packages/e7/64/66f2f4d1b4f0007c6e9078bd95b609b633d3957fe6dd23eac33ebde4b584/propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0", size = 241124, upload-time = "2025-03-26T03:04:08.676Z" }, - { url = "https://files.pythonhosted.org/packages/aa/bf/7b8c9fd097d511638fa9b6af3d986adbdf567598a567b46338c925144c1b/propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5", size = 240283, upload-time = "2025-03-26T03:04:10.172Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c9/e85aeeeaae83358e2a1ef32d6ff50a483a5d5248bc38510d030a6f4e2816/propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256", size = 232498, upload-time = "2025-03-26T03:04:11.616Z" }, - { url = "https://files.pythonhosted.org/packages/8e/66/acb88e1f30ef5536d785c283af2e62931cb934a56a3ecf39105887aa8905/propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073", size = 221486, upload-time = "2025-03-26T03:04:13.102Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f9/233ddb05ffdcaee4448508ee1d70aa7deff21bb41469ccdfcc339f871427/propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d", size = 222675, upload-time = "2025-03-26T03:04:14.658Z" }, - { url = "https://files.pythonhosted.org/packages/98/b8/eb977e28138f9e22a5a789daf608d36e05ed93093ef12a12441030da800a/propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f", size = 215727, upload-time = "2025-03-26T03:04:16.207Z" }, - { url = "https://files.pythonhosted.org/packages/89/2d/5f52d9c579f67b8ee1edd9ec073c91b23cc5b7ff7951a1e449e04ed8fdf3/propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0", size = 217878, upload-time = "2025-03-26T03:04:18.11Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fd/5283e5ed8a82b00c7a989b99bb6ea173db1ad750bf0bf8dff08d3f4a4e28/propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a", size = 230558, upload-time = "2025-03-26T03:04:19.562Z" }, - { url = "https://files.pythonhosted.org/packages/90/38/ab17d75938ef7ac87332c588857422ae126b1c76253f0f5b1242032923ca/propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a", size = 233754, upload-time = "2025-03-26T03:04:21.065Z" }, - { url = "https://files.pythonhosted.org/packages/06/5d/3b921b9c60659ae464137508d3b4c2b3f52f592ceb1964aa2533b32fcf0b/propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9", size = 226088, upload-time = "2025-03-26T03:04:22.718Z" }, - { url = "https://files.pythonhosted.org/packages/54/6e/30a11f4417d9266b5a464ac5a8c5164ddc9dd153dfa77bf57918165eb4ae/propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005", size = 40859, upload-time = "2025-03-26T03:04:24.039Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3a/8a68dd867da9ca2ee9dfd361093e9cb08cb0f37e5ddb2276f1b5177d7731/propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7", size = 45153, upload-time = "2025-03-26T03:04:25.211Z" }, - { url = "https://files.pythonhosted.org/packages/41/aa/ca78d9be314d1e15ff517b992bebbed3bdfef5b8919e85bf4940e57b6137/propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", size = 80430, upload-time = "2025-03-26T03:04:26.436Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d8/f0c17c44d1cda0ad1979af2e593ea290defdde9eaeb89b08abbe02a5e8e1/propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", size = 46637, upload-time = "2025-03-26T03:04:27.932Z" }, - { url = "https://files.pythonhosted.org/packages/ae/bd/c1e37265910752e6e5e8a4c1605d0129e5b7933c3dc3cf1b9b48ed83b364/propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", size = 46123, upload-time = "2025-03-26T03:04:30.659Z" }, - { url = "https://files.pythonhosted.org/packages/d4/b0/911eda0865f90c0c7e9f0415d40a5bf681204da5fd7ca089361a64c16b28/propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", size = 243031, upload-time = "2025-03-26T03:04:31.977Z" }, - { url = "https://files.pythonhosted.org/packages/0a/06/0da53397c76a74271621807265b6eb61fb011451b1ddebf43213df763669/propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", size = 249100, upload-time = "2025-03-26T03:04:33.45Z" }, - { url = "https://files.pythonhosted.org/packages/f1/eb/13090e05bf6b963fc1653cdc922133ced467cb4b8dab53158db5a37aa21e/propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", size = 250170, upload-time = "2025-03-26T03:04:35.542Z" }, - { url = "https://files.pythonhosted.org/packages/3b/4c/f72c9e1022b3b043ec7dc475a0f405d4c3e10b9b1d378a7330fecf0652da/propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", size = 245000, upload-time = "2025-03-26T03:04:37.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/fd/970ca0e22acc829f1adf5de3724085e778c1ad8a75bec010049502cb3a86/propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", size = 230262, upload-time = "2025-03-26T03:04:39.532Z" }, - { url = "https://files.pythonhosted.org/packages/c4/42/817289120c6b9194a44f6c3e6b2c3277c5b70bbad39e7df648f177cc3634/propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", size = 236772, upload-time = "2025-03-26T03:04:41.109Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9c/3b3942b302badd589ad6b672da3ca7b660a6c2f505cafd058133ddc73918/propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", size = 231133, upload-time = "2025-03-26T03:04:42.544Z" }, - { url = "https://files.pythonhosted.org/packages/98/a1/75f6355f9ad039108ff000dfc2e19962c8dea0430da9a1428e7975cf24b2/propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", size = 230741, upload-time = "2025-03-26T03:04:44.06Z" }, - { url = "https://files.pythonhosted.org/packages/67/0c/3e82563af77d1f8731132166da69fdfd95e71210e31f18edce08a1eb11ea/propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", size = 244047, upload-time = "2025-03-26T03:04:45.983Z" }, - { url = "https://files.pythonhosted.org/packages/f7/50/9fb7cca01532a08c4d5186d7bb2da6c4c587825c0ae134b89b47c7d62628/propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5", size = 246467, upload-time = "2025-03-26T03:04:47.699Z" }, - { url = "https://files.pythonhosted.org/packages/a9/02/ccbcf3e1c604c16cc525309161d57412c23cf2351523aedbb280eb7c9094/propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", size = 241022, upload-time = "2025-03-26T03:04:49.195Z" }, - { url = "https://files.pythonhosted.org/packages/db/19/e777227545e09ca1e77a6e21274ae9ec45de0f589f0ce3eca2a41f366220/propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", size = 40647, upload-time = "2025-03-26T03:04:50.595Z" }, - { url = "https://files.pythonhosted.org/packages/24/bb/3b1b01da5dd04c77a204c84e538ff11f624e31431cfde7201d9110b092b1/propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", size = 44784, upload-time = "2025-03-26T03:04:51.791Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" }, + { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207, upload-time = "2025-06-09T22:54:05.399Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648, upload-time = "2025-06-09T22:54:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496, upload-time = "2025-06-09T22:54:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288, upload-time = "2025-06-09T22:54:10.466Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456, upload-time = "2025-06-09T22:54:11.828Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429, upload-time = "2025-06-09T22:54:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472, upload-time = "2025-06-09T22:54:15.232Z" }, + { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480, upload-time = "2025-06-09T22:54:17.104Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530, upload-time = "2025-06-09T22:54:18.512Z" }, + { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230, upload-time = "2025-06-09T22:54:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754, upload-time = "2025-06-09T22:54:21.716Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430, upload-time = "2025-06-09T22:54:23.17Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884, upload-time = "2025-06-09T22:54:25.539Z" }, + { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480, upload-time = "2025-06-09T22:54:26.892Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757, upload-time = "2025-06-09T22:54:28.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500, upload-time = "2025-06-09T22:54:29.4Z" }, + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] [[package]] name = "protobuf" -version = "6.31.0" +version = "6.31.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/48/718c1e104a2e89970a8ff3b06d87e152834b576c570a6908f8c17ba88d65/protobuf-6.31.0.tar.gz", hash = "sha256:314fab1a6a316469dc2dd46f993cbbe95c861ea6807da910becfe7475bc26ffe", size = 441644, upload-time = "2025-05-14T17:58:27.862Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/77/8671682038b08237c927215fa3296bc1c54e4086fe542c87017c1b626663/protobuf-6.31.0-cp310-abi3-win32.whl", hash = "sha256:10bd62802dfa0588649740a59354090eaf54b8322f772fbdcca19bc78d27f0d6", size = 423437, upload-time = "2025-05-14T17:58:16.116Z" }, - { url = "https://files.pythonhosted.org/packages/e4/07/cc9b0cbf7593f6ef8cf87fa9b0e55cd74c5cb526dd89ad84aa7d6547ef8d/protobuf-6.31.0-cp310-abi3-win_amd64.whl", hash = "sha256:3e987c99fd634be8347246a02123250f394ba20573c953de133dc8b2c107dd71", size = 435118, upload-time = "2025-05-14T17:58:18.591Z" }, - { url = "https://files.pythonhosted.org/packages/21/46/33f884aa8bc59114dc97e0d954ca4618c556483670236008c88fbb7e834f/protobuf-6.31.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2c812f0f96ceb6b514448cefeb1df54ec06dde456783f5099c0e2f8a0f2caa89", size = 425439, upload-time = "2025-05-14T17:58:19.709Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f2/9a676b50229ce37b12777d7b21de90ae7bc0f9505d07e72e2e8d47b8d165/protobuf-6.31.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:67ce50195e4e584275623b8e6bc6d3d3dfd93924bf6116b86b3b8975ab9e4571", size = 321950, upload-time = "2025-05-14T17:58:22.04Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a7/243fa2d3c1b7675d54744b32dacf30356f4c27c0d3ad940ca8745a1c6b2c/protobuf-6.31.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:5353e38844168a327acd2b2aa440044411cd8d1b6774d5701008bd1dba067c79", size = 320904, upload-time = "2025-05-14T17:58:23.438Z" }, - { url = "https://files.pythonhosted.org/packages/ee/01/1ed1d482960a5718fd99c82f6d79120181947cfd4667ec3944d448ed44a3/protobuf-6.31.0-py3-none-any.whl", hash = "sha256:6ac2e82556e822c17a8d23aa1190bbc1d06efb9c261981da95c71c9da09e9e23", size = 168558, upload-time = "2025-05-14T17:58:26.923Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" }, + { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" }, + { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" }, ] [[package]] @@ -1802,11 +1777,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/e1/70/c7a4f46dbf06048c6 [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] @@ -1867,7 +1842,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/7d/ff/4c6f31a4f08979f12 [[package]] name = "pyobjc" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, @@ -1935,6 +1910,7 @@ dependencies = [ { name = "pyobjc-framework-fileproviderui", marker = "platform_release >= '19.0'" }, { name = "pyobjc-framework-findersync", marker = "platform_release >= '14.0'" }, { name = "pyobjc-framework-fsevents", marker = "platform_release >= '9.0'" }, + { name = "pyobjc-framework-fskit", marker = "platform_release >= '24.4'" }, { name = "pyobjc-framework-gamecenter", marker = "platform_release >= '12.0'" }, { name = "pyobjc-framework-gamecontroller", marker = "platform_release >= '13.0'" }, { name = "pyobjc-framework-gamekit", marker = "platform_release >= '12.0'" }, @@ -2003,6 +1979,7 @@ dependencies = [ { name = "pyobjc-framework-security" }, { name = "pyobjc-framework-securityfoundation" }, { name = "pyobjc-framework-securityinterface" }, + { name = "pyobjc-framework-securityui", marker = "platform_release >= '24.4'" }, { name = "pyobjc-framework-sensitivecontentanalysis", marker = "platform_release >= '23.0'" }, { name = "pyobjc-framework-servicemanagement", marker = "platform_release >= '10.0'" }, { name = "pyobjc-framework-sharedwithyou", marker = "platform_release >= '22.0'" }, @@ -2027,123 +2004,118 @@ dependencies = [ { name = "pyobjc-framework-vision", marker = "platform_release >= '17.0'" }, { name = "pyobjc-framework-webkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/d6/27b1c9a02f6cb4954984ce1a0239618e52f78c329c7e7450bf1f219b0f0a/pyobjc-11.0.tar.gz", hash = "sha256:a8f7baed65797f67afd46290b02f652c23f4b158ddf960bce0441b78f6803418", size = 11044, upload-time = "2025-01-14T19:02:12.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/5e/16bc372806790d295c76b5c7851767cc9ee3787b3e581f5d7cc44158e4e0/pyobjc-11.1.tar.gz", hash = "sha256:a71b14389657811d658526ba4d5faba4ef7eadbddcf9fe8bf4fb3a6261effba3", size = 11161, upload-time = "2025-06-14T20:56:32.819Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/55/d0971bccf8a5a347eaccf8caa4718766a68281baab83d2b5e211b2767504/pyobjc-11.0-py3-none-any.whl", hash = "sha256:3ed5e4e993192fd7fadd42a4148d266a3587af7453ea3b240bab724d02e34e64", size = 4169, upload-time = "2025-01-14T18:46:44.385Z" }, + { url = "https://files.pythonhosted.org/packages/a9/32/ad08b45fc0ad9850054ffe66fb0cb2ff7af3d2007c192dda14cf9a3ea893/pyobjc-11.1-py3-none-any.whl", hash = "sha256:903f822cba40be53d408b8eaf834514937ec0b4e6af1c5ecc24fcb652812dd85", size = 4164, upload-time = "2025-06-14T20:44:42.659Z" }, ] [[package]] name = "pyobjc-core" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/94/a111239b98260869780a5767e5d74bfd3a8c13a40457f479c28dcd91f89d/pyobjc_core-11.0.tar.gz", hash = "sha256:63bced211cb8a8fb5c8ff46473603da30e51112861bd02c438fbbbc8578d9a70", size = 994931, upload-time = "2025-01-14T19:02:13.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/e9/0b85c81e2b441267bca707b5d89f56c2f02578ef8f3eafddf0e0c0b8848c/pyobjc_core-11.1.tar.gz", hash = "sha256:b63d4d90c5df7e762f34739b39cc55bc63dbcf9fb2fb3f2671e528488c7a87fe", size = 974602, upload-time = "2025-06-14T20:56:34.189Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/05/fa97309c3b1bc1ec90d701db89902e0bd5e1024023aa2c5387b889458b1b/pyobjc_core-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:50675c0bb8696fe960a28466f9baf6943df2928a1fd85625d678fa2f428bd0bd", size = 727295, upload-time = "2025-01-14T18:46:50.208Z" }, - { url = "https://files.pythonhosted.org/packages/56/ce/bf3ff9a9347721a398c3dfb83e29b43fb166b7ef590f3f7b7ddcd283df39/pyobjc_core-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a03061d4955c62ddd7754224a80cdadfdf17b6b5f60df1d9169a3b1b02923f0b", size = 739750, upload-time = "2025-01-14T18:46:53.039Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a7/55afc166d89e3fcd87966f48f8bca3305a3a2d7c62100715b9ffa7153a90/pyobjc_core-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ec36680b5c14e2f73d432b03ba7c1457dc6ca70fa59fd7daea1073f2b4157d33", size = 671075, upload-time = "2025-06-14T20:44:46.594Z" }, + { url = "https://files.pythonhosted.org/packages/c0/09/e83228e878e73bf756749939f906a872da54488f18d75658afa7f1abbab1/pyobjc_core-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:765b97dea6b87ec4612b3212258024d8496ea23517c95a1c5f0735f96b7fd529", size = 677985, upload-time = "2025-06-14T20:44:48.375Z" }, ] [[package]] name = "pyobjc-framework-accessibility" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/61/7484cc4ad3aa7854cd4c969379a5f044261259d08f7c20b6718493b484f9/pyobjc_framework_accessibility-11.0.tar.gz", hash = "sha256:097450c641fa9ac665199762e77867f2a82775be2f749b8fa69223b828f60656", size = 44597, upload-time = "2025-01-14T19:02:17.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/10c16e9d48568a68da2f61866b19468d4ac7129c377d4b1333ee936ae5d0/pyobjc_framework_accessibility-11.1.tar.gz", hash = "sha256:c0fa5f1e00906ec002f582c7d3d80463a46d19f672bf5ec51144f819eeb40656", size = 45098, upload-time = "2025-06-14T20:56:35.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/2e/babcd02cd833c0aba34e10c34a2184021b8a3c7cb45d1ae806156c2b519d/pyobjc_framework_Accessibility-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c3a751d17b288bb56a98a10b52b253b3002c885fe686b604788acac1e9739437", size = 10948, upload-time = "2025-01-14T18:47:34.37Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ea/da3f982eeaffb80efb480892106caa19a2c9c8b8954570837ddbcd983520/pyobjc_framework_Accessibility-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:34536f3d60aeda618b384b1207a8c6f9978de278ce229c3469ef14fd27a3befa", size = 10962, upload-time = "2025-01-14T18:47:35.313Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c5/8803e4f9c3f2d3f5672097438e305be9ccfb87ad092c68cbf02b172bf1d2/pyobjc_framework_accessibility-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:332263153d829b946b311ddc8b9a4402b52d40a572b44c69c3242451ced1b008", size = 11135, upload-time = "2025-06-14T20:44:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bd/087d511e0ea356434399609a38e8819978943cbeaca3ca7cc5f35c93d0b2/pyobjc_framework_accessibility-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a049b63b32514da68aaaeef0d6c00a125e0618e4042aa6dbe3867b74fb2a8b2b", size = 11158, upload-time = "2025-06-14T20:44:59.032Z" }, ] [[package]] name = "pyobjc-framework-accounts" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/fa/b64f3f02e0a8b189dc07c391546e2dbe30ef1b3515d1427cdab743545b90/pyobjc_framework_accounts-11.0.tar.gz", hash = "sha256:afc4ae277be1e3e1f90269001c2fd886093a5465e365d7f9a3a0af3e17f06210", size = 17340, upload-time = "2025-01-14T19:02:18.625Z" } +sdist = { url = "https://files.pythonhosted.org/packages/12/45/ca21003f68ad0f13b5a9ac1761862ad2ddd83224b4314a2f7d03ca437c8d/pyobjc_framework_accounts-11.1.tar.gz", hash = "sha256:384fec156e13ff75253bb094339013f4013464f6dfd47e2f7de3e2ae7441c030", size = 17086, upload-time = "2025-06-14T20:56:36.035Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/45/5dfc72c82087d458ce7ddb17a338a38ae1848e72620537f31ed97192c65e/pyobjc_framework_Accounts-11.0-py2.py3-none-any.whl", hash = "sha256:3e4b494e1158e3250e4b4a09e9ff33b38f82d31aefe50dd47152c4a20ecdeec4", size = 5035, upload-time = "2025-01-14T18:47:40.92Z" }, - { url = "https://files.pythonhosted.org/packages/96/96/39b0cc9ced1180a93c75924a06598f24d0a7554b3e8ddfcb0828c0957476/pyobjc_framework_Accounts-11.0-py3-none-any.whl", hash = "sha256:ad0e378bd07ca7c88b45cda63b85424bc344e81ea44c0ae7327872d91cad311a", size = 5104, upload-time = "2025-01-14T18:47:41.967Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/fa1c4a964fb9f390af8fce1d82c053f9d4467ffe6acdaab464bb3220e673/pyobjc_framework_accounts-11.1-py2.py3-none-any.whl", hash = "sha256:9c3fe342be7b8e73cba735e5a38affbe349cf8bc19091aa4fd788eabf2074b72", size = 5117, upload-time = "2025-06-14T20:45:04.696Z" }, ] [[package]] name = "pyobjc-framework-addressbook" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/ef/5b5f6b61907ae43509fbf1654e043115d9a64d97efdc28fbb90d06c199f6/pyobjc_framework_addressbook-11.0.tar.gz", hash = "sha256:87073c85bb342eb27faa6eceb7a0e8a4c1e32ad1f2b62bb12dafb5e7b9f15837", size = 97116, upload-time = "2025-01-14T19:02:19.527Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/d3/f5bb5c72be5c6e52224f43e23e5a44e86d2c35ee9af36939e5514c6c7a0f/pyobjc_framework_addressbook-11.1.tar.gz", hash = "sha256:ce2db3be4a3128bf79d5c41319a6d16b73754785ce75ac694d0d658c690922fc", size = 97609, upload-time = "2025-06-14T20:56:37.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/7a/8b874a52ff57dad999330ac1f899e6df8e35cec2cad8a90d8002d3c5f196/pyobjc_framework_AddressBook-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:af7de23aac7571a3b9dad5b2881d8f186653aa72903db8d7dbfd2c7b993156b9", size = 13010, upload-time = "2025-01-14T18:47:47.925Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b4/93de1195c22cbaf4996aeb6d55e79fc7d76311cacfe8fd716c70fb20e391/pyobjc_framework_AddressBook-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3b634ef80920ab9208f2937527e4a498e7afa6e2ceb639ebb483387ab5b9accc", size = 13039, upload-time = "2025-01-14T18:47:49.509Z" }, + { url = "https://files.pythonhosted.org/packages/8f/46/27ade210b0bcf2903540c37e96f5e88ec5303e98dc12b255148f12ef9c04/pyobjc_framework_addressbook-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d1d69330b5a87a29d26feea95dcf40681fd00ba3b40ac89579072ce536b6b647", size = 13156, upload-time = "2025-06-14T20:45:06.788Z" }, + { url = "https://files.pythonhosted.org/packages/c2/de/e1ba5f113c05b543a097040add795fa4b85fdd5ad850b56d83cd6ce8afff/pyobjc_framework_addressbook-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb3d0a710f8342a0c63a8e4caf64a044b4d7e42d6d242c8e1b54470238b938cb", size = 13173, upload-time = "2025-06-14T20:45:07.755Z" }, ] [[package]] name = "pyobjc-framework-adservices" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/7c/0c6e01f83b0c5c7968564a40146f4d07080df278457bdb5a982c8f26a74d/pyobjc_framework_adservices-11.0.tar.gz", hash = "sha256:d2e1a2f395e93e1bbe754ab0d76ce1d64c0d3928472634437e0382eafc6765cd", size = 12732, upload-time = "2025-01-14T19:02:20.559Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/3f/af76eab6eee0a405a4fdee172e7181773040158476966ecd757b0a98bfc5/pyobjc_framework_adservices-11.1.tar.gz", hash = "sha256:44c72f8163705c9aa41baca938fdb17dde257639e5797e6a5c3a2b2d8afdade9", size = 12473, upload-time = "2025-06-14T20:56:38.147Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/10/601c9f5a07450ce75e166042d9ac5efe6286ac2d15212885a920260af9e3/pyobjc_framework_AdServices-11.0-py2.py3-none-any.whl", hash = "sha256:7cd1458f60175cd46bd88061c20e82f04b2576fc00bc5d54d67c18dcb870e27f", size = 3420, upload-time = "2025-01-14T18:47:42.812Z" }, - { url = "https://files.pythonhosted.org/packages/89/40/98a9116790e163d6c9ac0d19ce66307b03f9ac5ee64631db69899457b154/pyobjc_framework_AdServices-11.0-py3-none-any.whl", hash = "sha256:6426d4e4a43f5ee5ce7bab44d85647dbded3e17c0c62d8923cebaf927c4162ca", size = 3486, upload-time = "2025-01-14T18:47:43.845Z" }, + { url = "https://files.pythonhosted.org/packages/8e/11/a63a171ce86c25a6ae85ebff6a9ab92b0d0cb1fd66ddc7d7b0d803f36191/pyobjc_framework_adservices-11.1-py2.py3-none-any.whl", hash = "sha256:1744f59a75b2375e139c39f3e85658e62cd10cc0f12b158a80421f18734e9ffc", size = 3474, upload-time = "2025-06-14T20:45:13.263Z" }, ] [[package]] name = "pyobjc-framework-adsupport" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/07/b8b5f741d1e2cad97100444b255e6ecaca3668e7414039981799aa330035/pyobjc_framework_adsupport-11.0.tar.gz", hash = "sha256:20eb8a683d34fb7a6efeceaf964a24b88c3434875c44f66db5e1b609e678043a", size = 12819, upload-time = "2025-01-14T19:02:23.032Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/03/9c51edd964796a97def4e1433d76a128dd7059b685fb4366081bf4e292ba/pyobjc_framework_adsupport-11.1.tar.gz", hash = "sha256:78b9667c275785df96219d205bd4309731869c3298d0931e32aed83bede29096", size = 12556, upload-time = "2025-06-14T20:56:38.741Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/7f/2023c0a973f8823175c7e409fdbd306b275b0bb2723acf12ffade6ba5dbe/pyobjc_framework_AdSupport-11.0-py2.py3-none-any.whl", hash = "sha256:59161f5046def176d3aa6fdd6a05916029ca69ac69f836c67e0dd780a5efcf0f", size = 3334, upload-time = "2025-01-14T18:47:44.747Z" }, - { url = "https://files.pythonhosted.org/packages/cf/84/26c4275732952416603026888ca5462ed84372d412d0ccd7a1c750c01673/pyobjc_framework_AdSupport-11.0-py3-none-any.whl", hash = "sha256:91ba05eb5602911287bd04b0efefb7a485f9af255095b87c3e77bb7d1d1242ed", size = 3405, upload-time = "2025-01-14T18:47:45.767Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b8/ad895efb24311cab2b9d6f7f7f6a833b7f354f80fec606e6c7893da9349b/pyobjc_framework_adsupport-11.1-py2.py3-none-any.whl", hash = "sha256:c3e009612778948910d3a7135b9d77b9b7c06aab29d40957770834c083acf825", size = 3387, upload-time = "2025-06-14T20:45:14.394Z" }, ] [[package]] name = "pyobjc-framework-applescriptkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/c3/d7f9a33de7ab8e3950350e0862214e66f27ed6bff1a491bc391c377ab83e/pyobjc_framework_applescriptkit-11.0.tar.gz", hash = "sha256:4bafac4a036f0fb8ba01488b8e91d3ac861ce6e61154ffbd0b26f82b99779b50", size = 12638, upload-time = "2025-01-14T19:02:25.1Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/63/1bcfcdca53bf5bba3a7b4d73d24232ae1721a378a32fd4ebc34a35549df2/pyobjc_framework_applescriptkit-11.1.tar.gz", hash = "sha256:477707352eaa6cc4a5f8c593759dc3227a19d5958481b1482f0d59394a4601c3", size = 12392, upload-time = "2025-06-14T20:56:39.331Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/4b/5e7f6a182129be6f229ee6c036d84359b46b0f5f695824315c47b19d3149/pyobjc_framework_AppleScriptKit-11.0-py2.py3-none-any.whl", hash = "sha256:e8acc5ca99f5123ec4e60cb356c7cc407d5fe533ca53e5fa341b51f65495973b", size = 4246, upload-time = "2025-01-14T18:47:59.508Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ce/7965604f553c91fbd5602e17057b0935c100542abaf76291921335b6f75c/pyobjc_framework_AppleScriptKit-11.0-py3-none-any.whl", hash = "sha256:92cffd943a4d17f684bb51245744e9d0bb8992b2967125845dfeab09d26fc624", size = 4317, upload-time = "2025-01-14T18:48:02.221Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0e/68ac4ce71e613697a087c262aefacc9ed54eaf0cf1d9ffcd89134bfdab9b/pyobjc_framework_applescriptkit-11.1-py2.py3-none-any.whl", hash = "sha256:e22cbc9d1a25a4a713f21aa94dd017c311186b02062fc7ffbde3009495fb0067", size = 4334, upload-time = "2025-06-14T20:45:15.205Z" }, ] [[package]] name = "pyobjc-framework-applescriptobjc" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/9f/bb4fdbcea418f8472d7a67d4d2e4a15fca11fed04648db5208b0fce84807/pyobjc_framework_applescriptobjc-11.0.tar.gz", hash = "sha256:baff9988b6e886aed0e76441358417707de9088be5733f22055fed7904ca1001", size = 12675, upload-time = "2025-01-14T19:02:25.947Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/27/687b55b575367df045879b786f358355e40e41f847968e557d0718a6c4a4/pyobjc_framework_applescriptobjc-11.1.tar.gz", hash = "sha256:c8a0ec975b64411a4f16a1280c5ea8dbe949fd361e723edd343102f0f95aba6e", size = 12445, upload-time = "2025-06-14T20:56:39.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/7d/b3e28759df060f26a31407282e789a1a321612afcee3871134fdac8dc75f/pyobjc_framework_AppleScriptObjC-11.0-py2.py3-none-any.whl", hash = "sha256:a4c8d417fdb64180a283eadf8ddb804ba7f9e3cef149216a11b65e1d3509c55b", size = 4347, upload-time = "2025-01-14T18:48:03.193Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e7/c080a1cd77ce04e3bf4079a941105d3d670b9ba0fc91a54d4a1764bea02d/pyobjc_framework_AppleScriptObjC-11.0-py3-none-any.whl", hash = "sha256:681006b0cdf0279cd06b6d0f62b542b7f3b3b9b5d2391f7aa3798d8b355d67bf", size = 4416, upload-time = "2025-01-14T18:48:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/2d/33/ceb6a512b41fbf3458b9a281997ebb3056cc354981215261f0a2bf7d15d6/pyobjc_framework_applescriptobjc-11.1-py2.py3-none-any.whl", hash = "sha256:ac22526fd1f0a3b07ac1d77f90046b77f10ec9549182114f2428ee1e96d3de2b", size = 4433, upload-time = "2025-06-14T20:45:16.061Z" }, ] [[package]] name = "pyobjc-framework-applicationservices" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, @@ -2151,85 +2123,84 @@ dependencies = [ { name = "pyobjc-framework-coretext" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/fb/4e42573b0d3baa3fa18ec53614cf979f951313f1451e8f2e17df9429da1f/pyobjc_framework_applicationservices-11.0.tar.gz", hash = "sha256:d6ea18dfc7d5626a3ecf4ac72d510405c0d3a648ca38cae8db841acdebecf4d2", size = 224334, upload-time = "2025-01-14T19:02:26.828Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/3f/b33ce0cecc3a42f6c289dcbf9ff698b0d9e85f5796db2e9cb5dadccffbb9/pyobjc_framework_applicationservices-11.1.tar.gz", hash = "sha256:03fcd8c0c600db98fa8b85eb7b3bc31491701720c795e3f762b54e865138bbaf", size = 224842, upload-time = "2025-06-14T20:56:40.648Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/37/3d4dc6c004aaeb67bd43f7261d7c169ff45b8fc0eefbc7ba8cd6b0c881bc/pyobjc_framework_ApplicationServices-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61a99eef23abb704257310db4f5271137707e184768f6407030c01de4731b67b", size = 30846, upload-time = "2025-01-14T18:48:06.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/a9/7a45a67e126d32c61ea22ffd80e87ff7e05b4acf32bede6cce071fbfffc8/pyobjc_framework_ApplicationServices-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5fbeb425897d6129471d451ec61a29ddd5b1386eb26b1dd49cb313e34616ee21", size = 30908, upload-time = "2025-01-14T18:48:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/39/2d/9fde6de0b2a95fbb3d77ba11b3cc4f289dd208f38cb3a28389add87c0f44/pyobjc_framework_applicationservices-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cf45d15eddae36dec2330a9992fc852476b61c8f529874b9ec2805c768a75482", size = 30991, upload-time = "2025-06-14T20:45:18.169Z" }, + { url = "https://files.pythonhosted.org/packages/38/ec/46a5c710e2d7edf55105223c34fed5a7b7cc7aba7d00a3a7b0405d6a2d1a/pyobjc_framework_applicationservices-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f4a85ccd78bab84f7f05ac65ff9be117839dfc09d48c39edd65c617ed73eb01c", size = 31056, upload-time = "2025-06-14T20:45:18.925Z" }, ] [[package]] name = "pyobjc-framework-apptrackingtransparency" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/40/c1c48ed49b5e55c7a635aa1e7ca41ffa1c5547e26243f26489c4768cd730/pyobjc_framework_apptrackingtransparency-11.0.tar.gz", hash = "sha256:cd5c834b5b19c21ad6c317ba5d29f30a8d0ae5d14e7cf557da22abc0850f1e91", size = 13385, upload-time = "2025-01-14T19:02:29.226Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/68/7aa3afffd038dd6e5af764336bca734eb910121013ca71030457b61e5b99/pyobjc_framework_apptrackingtransparency-11.1.tar.gz", hash = "sha256:796cc5f83346c10973806cfb535d4200b894a5d2626ff2eeb1972d594d14fed4", size = 13135, upload-time = "2025-06-14T20:56:41.494Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/72/6e460cd763a3048c4d75769ed60a5af7832122b78224f710e40a9eb1c5cf/pyobjc_framework_AppTrackingTransparency-11.0-py2.py3-none-any.whl", hash = "sha256:1bf6d4f148d9f5d5befe90fcfd88ce988458a52719d53d5989b08e4fbed58864", size = 3805, upload-time = "2025-01-14T18:47:57.492Z" }, - { url = "https://files.pythonhosted.org/packages/33/cb/ef2622ee08349293aae6f81216cfee2423ad37d8a1d14ba4690b537d8850/pyobjc_framework_AppTrackingTransparency-11.0-py3-none-any.whl", hash = "sha256:347f876aea9d9f47d9fbf6dfa6d3f250ecd46f56a7c4616386327061e2ecc4e9", size = 3878, upload-time = "2025-01-14T18:47:58.595Z" }, + { url = "https://files.pythonhosted.org/packages/21/37/22cc0293c911a98a49c5fc007b968d82797101dd06e89c4c3266564ff443/pyobjc_framework_apptrackingtransparency-11.1-py2.py3-none-any.whl", hash = "sha256:e25c3eae25d24ee8b523b7ecc4d2b07af37c7733444b80c4964071dea7b0cb19", size = 3862, upload-time = "2025-06-14T20:45:23.851Z" }, ] [[package]] name = "pyobjc-framework-audiovideobridging" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/5f/0bd5beded0415b53f443da804410eda6a53e1bc64f8779ed9a592719da8c/pyobjc_framework_audiovideobridging-11.0.tar.gz", hash = "sha256:dbc45b06418dd780c365956fdfd69d007436b5ee54c51e671196562eb8290ba6", size = 72418, upload-time = "2025-01-14T19:02:30.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/25/6c5a7b1443d30139cc722029880284ea9dfa575f0436471b9364fcd499f5/pyobjc_framework_audiovideobridging-11.1.tar.gz", hash = "sha256:12756b3aa35083b8ad5c9139b6a0e2f4792e217096b5bf6b702d499038203991", size = 72913, upload-time = "2025-06-14T20:56:42.128Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/33/2ee33542febb40174d40ae8bbdf672b4e438a3fb41ba6a4d4a3e6800121b/pyobjc_framework_AudioVideoBridging-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d025e49ca6238be96d0a1c22942b548a8d445ef8eb71259b4769e119810f42c6", size = 10944, upload-time = "2025-01-14T18:48:11.978Z" }, - { url = "https://files.pythonhosted.org/packages/45/ea/db8295e17b0b544b06620e4019afcc76d7b743a8f03cb8a1024b2bc118ac/pyobjc_framework_AudioVideoBridging-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d414ecffeb23cddc8e64262af170e663c93e8d462d18aa7067d4584069967859", size = 10962, upload-time = "2025-01-14T18:48:12.953Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d0/952ccd59944f98f10f39c061ef7c3dceecbcd2654910e763c0ad2fd1c910/pyobjc_framework_audiovideobridging-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:db570433910d1df49cc45d25f7a966227033c794fb41133d59212689b86b1ac6", size = 11021, upload-time = "2025-06-14T20:45:25.498Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/3e8e3da4db835168d18155a2c90fcca441047fc9c2e021d2ea01b4c6eb8c/pyobjc_framework_audiovideobridging-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:591e80ff6973ea51a12f7c1a2e3fd59496633a51d5a1bf73f4fb989a43e23681", size = 11032, upload-time = "2025-06-14T20:45:26.196Z" }, ] [[package]] name = "pyobjc-framework-authenticationservices" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/0f/2de0d941e9c9b2eb1ce8b22eb31adc7227badfe1e53f615431d3a7fdcd48/pyobjc_framework_authenticationservices-11.0.tar.gz", hash = "sha256:6a060ce651df142e8923d1383449bc6f2c7f5eb0b517152dac609bde3901064e", size = 140036, upload-time = "2025-01-14T19:02:31.115Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/b7/3e9ad0ed3625dc02e495615ea5dbf55ca95cbd25b3e31f25092f5caad640/pyobjc_framework_authenticationservices-11.1.tar.gz", hash = "sha256:8fd801cdb53d426b4e678b0a8529c005d0c44f5a17ccd7052a7c3a1a87caed6a", size = 115266, upload-time = "2025-06-14T20:56:42.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/16/6246cf10e2d245f4018c02f351f8eb4cc93823f6e7e4dd584ab292cda786/pyobjc_framework_AuthenticationServices-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84e3b23478cf8995883acfe6c1a24503c84caf2f8dbe540377fe19fb787ce9b2", size = 20079, upload-time = "2025-01-14T18:48:19.023Z" }, - { url = "https://files.pythonhosted.org/packages/b7/ca/81a55a0714e73695b536bfbcbf0f5ddf68da9485b468406f6ef887a04938/pyobjc_framework_AuthenticationServices-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1779f72c264f749946fcbfba0575a985c1e297d426617739a533554dbf172f9a", size = 20105, upload-time = "2025-01-14T18:48:19.945Z" }, + { url = "https://files.pythonhosted.org/packages/31/99/0a9d2b9c1aa3b9713d322ddb90a59537013afdae5661af233409e7a24dc9/pyobjc_framework_authenticationservices-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3987b7fc9493c2ba77b773df99f6631bff1ee9b957d99e34afa6b4e1c9d48bfb", size = 20280, upload-time = "2025-06-14T20:45:32.617Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2d/cbb5e88c3713fb68cda7d76d37737076c1653bf1ac95418c30d4b614f4be/pyobjc_framework_authenticationservices-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6655dd53d9135ef85265a4297da5e7459ed7836973f2796027fdfbfd7f08e433", size = 20385, upload-time = "2025-06-14T20:45:33.359Z" }, ] [[package]] name = "pyobjc-framework-automaticassessmentconfiguration" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/d5/5febfee260b88e426c7e799cc95990818feeaa9f740fb9dd516559c96520/pyobjc_framework_automaticassessmentconfiguration-11.0.tar.gz", hash = "sha256:5d3691af2b94e44ca594b6791556e15a9f0a3f9432df51cb891f5f859a65e467", size = 24420, upload-time = "2025-01-14T19:02:32.101Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/39/d4c94e0245d290b83919854c4f205851cc0b2603f843448fdfb8e74aad71/pyobjc_framework_automaticassessmentconfiguration-11.1.tar.gz", hash = "sha256:70eadbf8600101901a56fcd7014d8941604e14f3b3728bc4fb0178a9a9420032", size = 24933, upload-time = "2025-06-14T20:56:43.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/3f/b593adce2f5b9f9427d784db56d8195adc2cfb340d4d3578914539a17faa/pyobjc_framework_AutomaticAssessmentConfiguration-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:25f2db399eb0a47e345d0471c7af930f5a3be899ba6edb40bd9125719e4b526f", size = 9015, upload-time = "2025-01-14T18:48:25.686Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c3/b6b779d783dcf3667a2011d8af0d801f6639df9735cdc34c6e6b79822298/pyobjc_framework_AutomaticAssessmentConfiguration-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b6433452d2c4cdb0eef16cc78a24ba9c61efb5bb04709ee10ca94b69119e889c", size = 9034, upload-time = "2025-01-14T18:48:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/f4ee1c9c274e0a41f8885f842fc78e520a367437edf9ca86eca46709e62d/pyobjc_framework_automaticassessmentconfiguration-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:50cc5466bec1f58f79921d49544b525b56897cb985dfcfabf825ee231c27bcfc", size = 9167, upload-time = "2025-06-14T20:45:39.52Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e0/5a67f8ee0393447ca8251cbd06788cb7f3a1f4b9b052afd2e1b2cdfcb504/pyobjc_framework_automaticassessmentconfiguration-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:55d1684dd676730fb1afbc7c67e0669e3a7159f18c126fea7453fe6182c098f9", size = 9193, upload-time = "2025-06-14T20:45:40.52Z" }, ] [[package]] name = "pyobjc-framework-automator" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/1b/1ba4eb296c3915f2e367e45470cb310a9c78b4dd65a37bd522f458f245aa/pyobjc_framework_automator-11.0.tar.gz", hash = "sha256:412d330f8c6f30066cad15e1bdecdc865510bbce469cc7d9477384c4e9f2550f", size = 200905, upload-time = "2025-01-14T19:02:33.039Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9f/097ed9f4de9e9491a1b08bb7d85d35a95d726c9e9f5f5bf203b359a436b6/pyobjc_framework_automator-11.1.tar.gz", hash = "sha256:9b46c55a4f9ae2b3c39ff560f42ced66bdd18c093188f0b5fc4060ad911838e4", size = 201439, upload-time = "2025-06-14T20:56:44.767Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/2b/bfe673491042849ad400bebf557b8047317757283b98ad9921fbb6f6cae9/pyobjc_framework_Automator-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:850f9641a54cc8d9a3d02c2d87a4e80aed2413b37aa6c26a7046088b77da5b42", size = 9811, upload-time = "2025-01-14T18:48:31.987Z" }, - { url = "https://files.pythonhosted.org/packages/13/00/e60db832c536fd354fab7e813ee781327358e6bcbc4cacbd9695dade7006/pyobjc_framework_Automator-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eb1b9b16873ec1d2f8af9a04ca1b2fcaa324ce4a1fada0d02fa239f6fecf773b", size = 9827, upload-time = "2025-01-14T18:48:32.958Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c0/ebcc5a041440625ca984cde4ff96bc3e2cac4e5a37ca5bf4506ef4a98c54/pyobjc_framework_automator-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bf675a19edd97de9c19dcfd0fea9af9ebbd3409786c162670d1d71cb2738e341", size = 10004, upload-time = "2025-06-14T20:45:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1e/3ed1df2168e596151da2329258951dae334e194d7de3b117c7e29a768ffc/pyobjc_framework_automator-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:af5941f8d90167244209b352512b7779e5590d17dc1e703e087a6cfe79ee3d64", size = 10029, upload-time = "2025-06-14T20:45:46.823Z" }, ] [[package]] name = "pyobjc-framework-avfoundation" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, @@ -2238,58 +2209,58 @@ dependencies = [ { name = "pyobjc-framework-coremedia" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/06/018ad0e2a38dbdbc5c126d7ce37488c4d581d4e2a2b9ef678162bb36d5f6/pyobjc_framework_avfoundation-11.0.tar.gz", hash = "sha256:269a592bdaf8a16948d8935f0cf7c8cb9a53e7ea609a963ada0e55f749ddb530", size = 871064, upload-time = "2025-01-14T19:02:35.757Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/1f/90cdbce1d3b4861cbb17c12adf57daeec32477eb1df8d3f9ab8551bdadfb/pyobjc_framework_avfoundation-11.1.tar.gz", hash = "sha256:6663056cc6ca49af8de6d36a7fff498f51e1a9a7f1bde7afba718a8ceaaa7377", size = 832178, upload-time = "2025-06-14T20:56:46.329Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/b5/327654548fa210b4637350de016183fbb1f6f8f9213d2c6c9b492eb8b44c/pyobjc_framework_AVFoundation-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87db350311c1d7e07d68036cdde3d01c09d97b8ba502241c0c1699d7a9c6f2e4", size = 71345, upload-time = "2025-01-14T18:47:04.589Z" }, - { url = "https://files.pythonhosted.org/packages/f5/36/e09b20f280953fa7be95a9266e5ad75f2e8b184cc39260c0537b3e60b534/pyobjc_framework_AVFoundation-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6bb6f4be53c0fb42bee3f46cf0bb5396a8fd13f92d47a01f6b77037a1134f26b", size = 71314, upload-time = "2025-01-14T18:47:05.616Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/31286b2b09a619d8047256d7180e0d511be71ab598e5f54f034977b59bbf/pyobjc_framework_avfoundation-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8a0ccbdba46b69dec1d12eea52eef56fcd63c492f73e41011bb72508b2aa2d0e", size = 70711, upload-time = "2025-06-14T20:45:52.461Z" }, + { url = "https://files.pythonhosted.org/packages/43/30/d5d03dd4a508bdaa2156ff379e9e109020de23cbb6316c5865d341aa6db1/pyobjc_framework_avfoundation-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94f065db4e87b1baebb5cf9f464cf9d82c5f903fff192001ebc974d9e3132c7e", size = 70746, upload-time = "2025-06-14T20:45:53.253Z" }, ] [[package]] name = "pyobjc-framework-avkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/79/5b2fcb94b051da32a24b54bb0d90b1d01b190e1402b6303747de47fb17ac/pyobjc_framework_avkit-11.0.tar.gz", hash = "sha256:5fa40919320277b820df3e4c6e84cba91ef7221a28f4eb5374e3dbd80d1e521a", size = 46311, upload-time = "2025-01-14T19:02:37.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/ff/9f41f2b8de786871184b48c4e5052cb7c9fcc204e7fee06687fa32b08bed/pyobjc_framework_avkit-11.1.tar.gz", hash = "sha256:d948204a7b94e0e878b19a909f9b33342e19d9ea519571d66a21fce8f72e3263", size = 46825, upload-time = "2025-06-14T20:56:47.494Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/ae/aed1023150483a288922c447e1997f4f4e9d0460038e1a070a3a53b85c19/pyobjc_framework_AVKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:16b5560860c1e13e692c677ad04d8e194d0b9931dd3f15e3df4dbd7217cc8ab1", size = 11960, upload-time = "2025-01-14T18:47:15.738Z" }, - { url = "https://files.pythonhosted.org/packages/01/f4/08684e5af2a2e8940e6411e96ef1d7ed1e51a121abb19c93c25c34969213/pyobjc_framework_AVKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f4da468b97bb7f356024e31647619cd1cd435b543e467209da0ee0abdfdc7121", size = 11969, upload-time = "2025-01-14T18:47:16.602Z" }, + { url = "https://files.pythonhosted.org/packages/a4/6c/ee7504367f4a9337d3e78cd34beb9fcb58ad30e274c2a9f1d8058b9837f2/pyobjc_framework_avkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:88f70e2a399e43ce7bc3124b3b35d65537daddb358ea542fbb0146fa6406be8a", size = 11517, upload-time = "2025-06-14T20:45:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/6ec6a4ec7eb9ca329f36bbd2a51750fe5064d44dd437d8615abb7121ec93/pyobjc_framework_avkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ef9cd9fe37c6199bfde7ee5cd6e76ede23a6797932882785c53ef3070e209afb", size = 11539, upload-time = "2025-06-14T20:46:00.375Z" }, ] [[package]] name = "pyobjc-framework-avrouting" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/80/63680dc7788bc3573a20fc5421dfcf606970a0cd3b2457829d9b66603ae0/pyobjc_framework_avrouting-11.0.tar.gz", hash = "sha256:54ec9ea0b5adb5149b554e23c07c6b4f4bdb2892ca2ed7b3e88a5de936313025", size = 20561, upload-time = "2025-01-14T19:02:38.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/42/94bc18b968a4ee8b6427257f907ffbfc97f8ba6a6202953da149b649d638/pyobjc_framework_avrouting-11.1.tar.gz", hash = "sha256:7db1291d9f53cc58d34b2a826feb721a85f50ceb5e71952e8762baacd3db3fc0", size = 21069, upload-time = "2025-06-14T20:56:48.57Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/9c/ea6924de09e13f858210d6dd934f00773b1e3db6af886c72841ed545560f/pyobjc_framework_AVRouting-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54e58cd0292f734aba035599f37a0c00f03761e9ff5cf53a0857cec7949bb39c", size = 8067, upload-time = "2025-01-14T18:47:26.354Z" }, - { url = "https://files.pythonhosted.org/packages/3f/92/774e10af5aba5742c4a2dd563fa819550d9caa755d2648b3cc87bbe30129/pyobjc_framework_AVRouting-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:779db3fb0048b22c5dcf5871930025c0fd93068f87946e8053f31a3366fa6fb0", size = 8078, upload-time = "2025-01-14T18:47:28.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/0d17fd5a761d8a3d7dab0e096315de694b47dd48d2bb9655534e44399385/pyobjc_framework_avrouting-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45cbabbf69764b2467d78adb8f3b7f209d1a8ee690e19f9a32d05c62a9c3a131", size = 8192, upload-time = "2025-06-14T20:46:05.479Z" }, + { url = "https://files.pythonhosted.org/packages/01/17/ce199bc7fb3ba1f7b0474554bd71d1bdd3d5a141e1d9722ff9f46c104e1d/pyobjc_framework_avrouting-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc309e175abf3961f933f8b341c0504b17f4717931242ebb121a83256b8b5c13", size = 8212, upload-time = "2025-06-14T20:46:06.17Z" }, ] [[package]] name = "pyobjc-framework-backgroundassets" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/17/83b873069b0c0763365de88648ad4a2472e9e96fcac39fa534f3633552e8/pyobjc_framework_backgroundassets-11.0.tar.gz", hash = "sha256:9488c3f86bf427898a88b7100e77200c08a487a35c75c1b5735bd69c57ba38cb", size = 23658, upload-time = "2025-01-14T19:02:42.665Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/76/21e1632a212f997d7a5f26d53eb997951978916858039b79f43ebe3d10b2/pyobjc_framework_backgroundassets-11.1.tar.gz", hash = "sha256:2e14b50539d96d5fca70c49f21b69fdbad81a22549e3630f5e4f20d5c0204fc2", size = 24803, upload-time = "2025-06-14T20:56:49.566Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/9d/bea4408649199340ec7ed154bbaa1942a0b0955006b3153088b3f35e6ff6/pyobjc_framework_BackgroundAssets-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:812bcc4eaf71c1cc42e94edc2b5ad0414d16cfe1da5c421edd9382417d625f06", size = 9499, upload-time = "2025-01-14T18:48:39.156Z" }, - { url = "https://files.pythonhosted.org/packages/bd/79/726c14fd26553c8bbe8b2ed55caa45d89093e2e85b45c1b598dd04ea7589/pyobjc_framework_BackgroundAssets-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:96b3fc40c514867d4a0b3ad4d256bc5134d789e22fa306a6b21e49ecadc51698", size = 9521, upload-time = "2025-01-14T18:48:40.063Z" }, + { url = "https://files.pythonhosted.org/packages/74/ac/b1cb5c0ec2691ea225d53c2b9411d5ea1896f8f72eb5ca92978664443bb0/pyobjc_framework_backgroundassets-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bd371ce08d1b79f540d5994139898097b83b1d4e4471c264892433d448b24de0", size = 9691, upload-time = "2025-06-14T20:46:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/a6ad2df35fd71b3c26f52698d25174899ba1be134766022f5bf804ebf12d/pyobjc_framework_backgroundassets-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:13bf451c59b409b6ce1ac0e717a970a1b03bca7a944a7f19219da0d46ab7c561", size = 9707, upload-time = "2025-06-14T20:46:12.88Z" }, ] [[package]] name = "pyobjc-framework-browserenginekit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, @@ -2298,85 +2269,82 @@ dependencies = [ { name = "pyobjc-framework-coremedia" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/2e/df3d2f7e53132d398c2922d331dd1d2aa352997a1a4a1390e59db51c1d13/pyobjc_framework_browserenginekit-11.0.tar.gz", hash = "sha256:51971527f5103c0e09a4ef438c352ebb037fcad8971f8420a781c72ee421f758", size = 31352, upload-time = "2025-01-14T19:02:45.499Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/75/087270d9f81e913b57c7db58eaff8691fa0574b11faf9302340b3b8320f1/pyobjc_framework_browserenginekit-11.1.tar.gz", hash = "sha256:918440cefb10480024f645169de3733e30ede65e41267fa12c7b90c264a0a479", size = 31944, upload-time = "2025-06-14T20:56:50.195Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/6d/6aa929d4993453817523db9c82a4e6e2cce7104fa59e29ee857f9e926b0d/pyobjc_framework_BrowserEngineKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:58494bc3ccc21a63751b7c9f8788d0240c3f1aad84cf221c0e42c9764a069ba4", size = 10913, upload-time = "2025-01-14T18:48:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2f/dd18f7ff9438ad4612febfbdb2e4bded37033347b9f0e1355df76f2c5213/pyobjc_framework_BrowserEngineKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0925edfd60a24f53819cfd11f07926fd42bc0fbeb7a4982998a08742e859dbff", size = 10933, upload-time = "2025-01-14T18:48:45.673Z" }, + { url = "https://files.pythonhosted.org/packages/ea/29/ec0a0cc6fb15911769cb8e5ad8ada85e3f5cf4889fafbb90d936c6b7053b/pyobjc_framework_browserenginekit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:29b5f5949170af0235485e79aa465a7af2b2e0913d0c2c9ab1ac033224a90edb", size = 11088, upload-time = "2025-06-14T20:46:18.696Z" }, + { url = "https://files.pythonhosted.org/packages/89/90/a50bb66a5e041ace99b6c8b1df43b38d5f2e1bf771f57409e4aebf1dfae5/pyobjc_framework_browserenginekit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9b815b167533015d62832b956e9cfb962bd2026f5a4ccd66718cf3bb2e15ab27", size = 11115, upload-time = "2025-06-14T20:46:19.401Z" }, ] [[package]] name = "pyobjc-framework-businesschat" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/f2/4541989f2c9c5fc3cdfc94ebf31fc6619554b6c22dafdbb57f866a392bc1/pyobjc_framework_businesschat-11.0.tar.gz", hash = "sha256:20fe1c8c848ef3c2e132172d9a007a8aa65b08875a9ca5c27afbfc4396b16dbb", size = 12953, upload-time = "2025-01-14T19:02:46.378Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/be/9d9d9d9383c411a58323ea510d768443287ca21610af652b815b3205ea80/pyobjc_framework_businesschat-11.1.tar.gz", hash = "sha256:69589d2f0cb4e7892e5ecc6aed79b1abd1ec55c099a7faacae6a326bc921259d", size = 12698, upload-time = "2025-06-14T20:56:51.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/5b/d7313368ea4056092400c7a4ed5c705d3d21a443641d98b140054edbd930/pyobjc_framework_BusinessChat-11.0-py2.py3-none-any.whl", hash = "sha256:1f732fdace31d2abdd14b3054f27a5e0f4591c7e1bef069b6aeb4f9c8d9ec487", size = 3408, upload-time = "2025-01-14T18:48:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/8a/e6/c82e2eb2b4ad4407f1ada6d41ef583eb211cce88ffcc2e05c826760f721d/pyobjc_framework_BusinessChat-11.0-py3-none-any.whl", hash = "sha256:47a2e4da9b061daa89a6367cb0e6bb8cdea0627379dd6d5095a8fd20243d8613", size = 3477, upload-time = "2025-01-14T18:48:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/87/a4/5b8bb268b263678c0908cdaa8bed2534a6caac5862d05236f6c361d130ba/pyobjc_framework_businesschat-11.1-py2.py3-none-any.whl", hash = "sha256:7fdc1219b988ce3ae896bffd01f547c06cec3b4e4b2d0aa04d251444d7f1c2db", size = 3458, upload-time = "2025-06-14T20:46:24.651Z" }, ] [[package]] name = "pyobjc-framework-calendarstore" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/d3/722c1b16c7d9bdd5c408735c15193e8396f2d22ab6410b0af4569f39c46e/pyobjc_framework_calendarstore-11.0.tar.gz", hash = "sha256:40173f729df56b70ec14f9680962a248c3ce7b4babb46e8b0d760a13975ef174", size = 68475, upload-time = "2025-01-14T19:02:48.544Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/df/7ca8ee65b16d5fc862d7e8664289472eed918cf4d76921de6bdaa1461c65/pyobjc_framework_calendarstore-11.1.tar.gz", hash = "sha256:858ee00e6a380d9c086c2d7db82c116a6c406234038e0ec8fc2ad02e385dc437", size = 68215, upload-time = "2025-06-14T20:56:51.799Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/e1/02bda98aae43957943adb09700265603f8ff8ff2197e57b082237a8e1a8f/pyobjc_framework_CalendarStore-11.0-py2.py3-none-any.whl", hash = "sha256:67ddc18c96bba42118fc92f1117b053c58c8888edb74193f0be67a10051cc9e2", size = 5183, upload-time = "2025-01-14T18:49:01.649Z" }, - { url = "https://files.pythonhosted.org/packages/a2/5b/922df21b738e8d349df27b2a73eaf8bba93c84c8c4d0d133fdd5de2ff236/pyobjc_framework_CalendarStore-11.0-py3-none-any.whl", hash = "sha256:9b310fe66ac12e0feb7c8e3166034bec357a45f7f8b8916e93eddc6f199d08c8", size = 5251, upload-time = "2025-01-14T18:49:03.224Z" }, + { url = "https://files.pythonhosted.org/packages/c7/94/69cb863bd88349df0f6cf491fd3ca4d674816c4d66270f9e2620cc6e16ed/pyobjc_framework_calendarstore-11.1-py2.py3-none-any.whl", hash = "sha256:bf066e17392c978becf17a61863eb81727bf593a2bfdab261177126072557e24", size = 5265, upload-time = "2025-06-14T20:46:25.457Z" }, ] [[package]] name = "pyobjc-framework-callkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/0a/9d39ebac92006960b8059f664d8eb7b9cdb8763fe4e8102b2d24b853004f/pyobjc_framework_callkit-11.0.tar.gz", hash = "sha256:52e44a05d0357558e1479977ed2bcb325fabc8d337f641f0249178b5b491fc59", size = 39720, upload-time = "2025-01-14T19:02:50.697Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/d5/4f0b62ab35be619e8c8d96538a03cf56fde6fd53540e1837e0fa588b3f6c/pyobjc_framework_callkit-11.1.tar.gz", hash = "sha256:b84d5ea38dff0cbe0754f5f9f6f33c742e216f12e7166179a8ec2cf4b0bfca94", size = 46648, upload-time = "2025-06-14T20:56:52.579Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/86/8d7dc24702ae810b6230d8b2cebb1c31e12abc31507095b1a9655715c921/pyobjc_framework_CallKit-11.0-py2.py3-none-any.whl", hash = "sha256:f19d94b61ecd981f4691fd244f536f947687b872ac793ccc2b3122b3854e887a", size = 5248, upload-time = "2025-01-14T18:49:05.438Z" }, - { url = "https://files.pythonhosted.org/packages/25/bd/ff89f7e5438c767fc43f603bee42a447315be48a09f64b9aa4da719ecdfc/pyobjc_framework_CallKit-11.0-py3-none-any.whl", hash = "sha256:95394b7f7a50916debe4f7a884ce9135d11733a14e07a8c502171e77bd0087a4", size = 5314, upload-time = "2025-01-14T18:49:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f8/6e368225634cad9e457c4f8f0580ed318cb2f2c8110f2e56935fc12502f3/pyobjc_framework_callkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1db8b74abd6489d73c8619972730bea87a7d1f55d47649150fc1a30fdc6840fb", size = 11211, upload-time = "2025-06-14T20:46:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/18/2a/209572a6dba6768a57667e1f87a83ce8cadf18de5d6b1a91b95ce548d0f8/pyobjc_framework_callkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:554e09ca3dab44d93a89927d9e300f004d2ef0db020b10425a4622b432e7b684", size = 11269, upload-time = "2025-06-14T20:46:28.164Z" }, ] [[package]] name = "pyobjc-framework-carbon" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/15/51964f36a8ae1002b16d213d2e5ba11cc861bdd9369f1e3f116350d788c5/pyobjc_framework_carbon-11.0.tar.gz", hash = "sha256:476f690f0b34aa9e4cb3923e61481aefdcf33e38ec6087b530a94871eee2b914", size = 37538, upload-time = "2025-01-14T19:02:51.62Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/a4/d751851865d9a78405cfec0c8b2931b1e96b9914e9788cd441fa4e8290d0/pyobjc_framework_carbon-11.1.tar.gz", hash = "sha256:047f098535479efa3ab89da1ebdf3cf9ec0b439a33a4f32806193886e9fcea71", size = 37291, upload-time = "2025-06-14T20:56:53.642Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/fb/e5724934c3a2bbed4fbda4230e15a8b7b86313b39491876647300cb4fb11/pyobjc_framework_Carbon-11.0-py2.py3-none-any.whl", hash = "sha256:beef5095269d8e5427e09f9687963515c1b79fbf6927ff756a8414445892987d", size = 4700, upload-time = "2025-01-14T18:49:07.341Z" }, - { url = "https://files.pythonhosted.org/packages/1a/3d/b53c2d8949067f3f45491e250620e437569f1b4e6a028f2f5e721726283e/pyobjc_framework_Carbon-11.0-py3-none-any.whl", hash = "sha256:9a269042e8f5705897ac64d2b48515ba055462c88460cf140f5d8d4b8c806a42", size = 4768, upload-time = "2025-01-14T18:49:10.256Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f1a20b5aa3833af4d461074c479263a410ef90d17dbec11f78ad9c34dbab/pyobjc_framework_carbon-11.1-py2.py3-none-any.whl", hash = "sha256:1bf66853e939315ad7ee968170b16dd12cb838c42b80dfcd5354687760998825", size = 4753, upload-time = "2025-06-14T20:46:33.141Z" }, ] [[package]] name = "pyobjc-framework-cfnetwork" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4f/36/7cebdfb621c7d46eeab3173256bc2e1cba1bbbbe6c0ac8aeb9a4fe2a4627/pyobjc_framework_cfnetwork-11.0.tar.gz", hash = "sha256:eb742fc6a42b248886ff09c3cf247d56e65236864bbea4264e70af8377948d96", size = 78532, upload-time = "2025-01-14T19:02:52.777Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/49/7b24172e3d6eb0ddffc33a7498a2bea264aa2958c3fecaeb463bef88f0b8/pyobjc_framework_cfnetwork-11.1.tar.gz", hash = "sha256:ad600163eeadb7bf71abc51a9b6f2b5462a018d3f9bb1510c5ce3fdf2f22959d", size = 79069, upload-time = "2025-06-14T20:56:54.615Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/85/11047cfe2d31c242694d780783f0dea81d73cbb09929c7d4b918ce2d29bf/pyobjc_framework_CFNetwork-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e6905c86ccb5608f4153aacb931758ad39af8b708fcebb497431f9741f39e6d", size = 18988, upload-time = "2025-01-14T18:48:54.869Z" }, - { url = "https://files.pythonhosted.org/packages/3e/6e/7d90c329030e7dd6ebbec217434820ff6158a3af9906e2abbb43e9b685d6/pyobjc_framework_CFNetwork-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5f61010503073e3518e29d440079a7c0b40aef91be6d3c2032e492c21bada80b", size = 19144, upload-time = "2025-01-14T18:48:55.864Z" }, + { url = "https://files.pythonhosted.org/packages/e7/61/74b0d0430807615b7f91a688a871ffd94a61d4764a101e2a53e0c95dd05e/pyobjc_framework_cfnetwork-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7a24746d0754b3a0042def2cd64aa205e5614f12ea0de9461c8e26d97633c72", size = 18953, upload-time = "2025-06-14T20:46:35.409Z" }, + { url = "https://files.pythonhosted.org/packages/c2/31/05b4fb79e7f738f7f7d7a58734de2fab47d9a1fb219c2180e8c07efe2550/pyobjc_framework_cfnetwork-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:70beb8095df76e0e8eb7ab218be1e69ae180e01a4d77f7cad73c97b4eb7a296a", size = 19141, upload-time = "2025-06-14T20:46:36.134Z" }, ] [[package]] name = "pyobjc-framework-cinematic" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, @@ -2385,29 +2353,28 @@ dependencies = [ { name = "pyobjc-framework-coremedia" }, { name = "pyobjc-framework-metal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/ef/b5857d567cd6e0366f61c381ebea52383b98d1ac03341f39e779a085812a/pyobjc_framework_cinematic-11.0.tar.gz", hash = "sha256:94a2de8bf3f38bd190311b6bf98d1e2cea7888840b3ce3aa92e464c0216a5cdb", size = 25740, upload-time = "2025-01-14T19:02:54.95Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/6f/c2d0b49e01e654496a1781bafb9da72a6fbd00f5abb39dc4a3a0045167c7/pyobjc_framework_cinematic-11.1.tar.gz", hash = "sha256:efde39a6a2379e1738dbc5434b2470cd187cf3114ffb81390b3b1abda470b382", size = 25522, upload-time = "2025-06-14T20:56:55.379Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/cf/a60e131bddf5cced32a3c0050d264f2255d63c45be398cede1db03ea8b51/pyobjc_framework_Cinematic-11.0-py2.py3-none-any.whl", hash = "sha256:281721969978d726ded9bae38c4acd6713495c399025ff2b4179fc02ec68b336", size = 4508, upload-time = "2025-01-14T18:49:11.202Z" }, - { url = "https://files.pythonhosted.org/packages/09/a8/4ea347c1fc5774e2bbe7bb688fc625d583103d1e212f7b896ed19d14844b/pyobjc_framework_Cinematic-11.0-py3-none-any.whl", hash = "sha256:3a24f3528d7f77637f51fd1862cc8c79e4d0da4ba6fd3dd02b54adddec365826", size = 4580, upload-time = "2025-01-14T18:49:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/05/bd/a9b51c770bd96546a101c9e9994f851b87336f168a77048241517ca4db8c/pyobjc_framework_cinematic-11.1-py2.py3-none-any.whl", hash = "sha256:b62c024c1a9c7890481bc2fdfaf0cd3c251a4a08357d57dc1795d98920fcdbd1", size = 4562, upload-time = "2025-06-14T20:46:40.989Z" }, ] [[package]] name = "pyobjc-framework-classkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/81/126075eaf5ccf254ddb4cfd99d92a266c30803c5b4572ea3a920fd85e850/pyobjc_framework_classkit-11.0.tar.gz", hash = "sha256:dc5b3856612cafdc7071fbebc252b8908dbf2433e0e5ddb15a0bcd1ee282d27c", size = 39301, upload-time = "2025-01-14T19:02:55.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/8b/5150b4faddd15d5dd795bc62b2256c4f7dafc983cfa694fcf88121ea0016/pyobjc_framework_classkit-11.1.tar.gz", hash = "sha256:ee1e26395eb00b3ed5442e3234cdbfe925d2413185af38eca0477d7166651df4", size = 39831, upload-time = "2025-06-14T20:56:56.036Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/77/2e31bcf1e9b63f6723c01329c1191ac163e79b0f548b7cd92414115c26ff/pyobjc_framework_ClassKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:723a07591e1e40380c339b58033e8491e58be4080c0f77a26be0728f1c5025c8", size = 8776, upload-time = "2025-01-14T18:49:14.05Z" }, - { url = "https://files.pythonhosted.org/packages/68/87/f566c4f1ffd1e383c7b38cd22753dfef0863f30bfdb0b3c5102293057fc2/pyobjc_framework_ClassKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7c7ff2eb8a9d87cb69618668e96c41ed9467fd4b4a8fef517c49923c0f6418e6", size = 8794, upload-time = "2025-01-14T18:49:14.985Z" }, + { url = "https://files.pythonhosted.org/packages/89/86/5b9ef1d5aa3f4835d164c9be46afae634911db56c6ad7795e212ef9bb50b/pyobjc_framework_classkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:018da363d06f3615c07a8623cbdb024a31b1f8b96a933ff2656c0e903063842c", size = 8895, upload-time = "2025-06-14T20:46:42.689Z" }, + { url = "https://files.pythonhosted.org/packages/75/79/2552fd5e1da73dffb35589469b3cd8c0928e3100462761350d19ea922e59/pyobjc_framework_classkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:161dcb9b718649e6331a5eab5a76c2b43a9b322b15b37b3f8f9c5faad12ee6d1", size = 8911, upload-time = "2025-06-14T20:46:43.714Z" }, ] [[package]] name = "pyobjc-framework-cloudkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, @@ -2416,893 +2383,880 @@ dependencies = [ { name = "pyobjc-framework-coredata" }, { name = "pyobjc-framework-corelocation" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/6c/b0709fed7fc5a1e81de311b9273bb7ba3820a636f8ba880e90510bb6d460/pyobjc_framework_cloudkit-11.0.tar.gz", hash = "sha256:e3f6bf2c3358dd394174b1e69fcec6859951fcd15f6433c6fa3082e3b7e2656d", size = 123034, upload-time = "2025-01-14T19:02:56.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/a6/bfe5be55ed95704efca0e86b218155a9c801735107cedba3af8ea4580a05/pyobjc_framework_cloudkit-11.1.tar.gz", hash = "sha256:40d2dc4bf28c5be9b836b01e4d267a15d847d756c2a65530e1fcd79b2825e86d", size = 122778, upload-time = "2025-06-14T20:56:56.73Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/db/9f914422be88eb2c917d67aebac9dde2e272ea1b510ca1e0db17a09db125/pyobjc_framework_CloudKit-11.0-py2.py3-none-any.whl", hash = "sha256:10cb153d7185dd260d21596f75fca8502236f6afd8e72e866cff8acd9c025f14", size = 10785, upload-time = "2025-01-14T18:49:21.369Z" }, - { url = "https://files.pythonhosted.org/packages/53/73/239581763a1bd56475ebd9bdde52a79cf0b6cac20b3d4442283b1ef8705c/pyobjc_framework_CloudKit-11.0-py3-none-any.whl", hash = "sha256:b2376d92d5822ce7e4feefcffdc3f4d1d230929f1735793da6d36b52b161b552", size = 10854, upload-time = "2025-01-14T18:49:23.612Z" }, + { url = "https://files.pythonhosted.org/packages/25/d9/5570a217cef8130708e860b86f4f22bb5827247c97121523a9dfd4784148/pyobjc_framework_cloudkit-11.1-py2.py3-none-any.whl", hash = "sha256:c583e40c710cf85ebe34173d1d2995e832a20127edc8899b2f35b13f98498af1", size = 10870, upload-time = "2025-06-14T20:46:48.781Z" }, ] [[package]] name = "pyobjc-framework-cocoa" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/32/53809096ad5fc3e7a2c5ddea642590a5f2cb5b81d0ad6ea67fdb2263d9f9/pyobjc_framework_cocoa-11.0.tar.gz", hash = "sha256:00346a8cb81ad7b017b32ff7bf596000f9faa905807b1bd234644ebd47f692c5", size = 6173848, upload-time = "2025-01-14T19:03:00.125Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/c5/7a866d24bc026f79239b74d05e2cf3088b03263da66d53d1b4cf5207f5ae/pyobjc_framework_cocoa-11.1.tar.gz", hash = "sha256:87df76b9b73e7ca699a828ff112564b59251bb9bbe72e610e670a4dc9940d038", size = 5565335, upload-time = "2025-06-14T20:56:59.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/97/81fd41ad90e9c241172110aa635a6239d56f50d75923aaedbbe351828580/pyobjc_framework_Cocoa-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3ea7be6e6dd801b297440de02d312ba3fa7fd3c322db747ae1cb237e975f5d33", size = 385534, upload-time = "2025-01-14T18:49:27.898Z" }, - { url = "https://files.pythonhosted.org/packages/5b/8d/0e2558447c26b3ba64f7c9776a5a6c9d2ae8abf9d34308b174ae0934402e/pyobjc_framework_Cocoa-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:280a577b83c68175a28b2b7138d1d2d3111f2b2b66c30e86f81a19c2b02eae71", size = 385811, upload-time = "2025-01-14T18:49:29.259Z" }, + { url = "https://files.pythonhosted.org/packages/90/43/6841046aa4e257b6276cd23e53cacedfb842ecaf3386bb360fa9cc319aa1/pyobjc_framework_cocoa-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b9a9b8ba07f5bf84866399e3de2aa311ed1c34d5d2788a995bdbe82cc36cfa0", size = 388177, upload-time = "2025-06-14T20:46:51.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/da/41c0f7edc92ead461cced7e67813e27fa17da3c5da428afdb4086c69d7ba/pyobjc_framework_cocoa-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806de56f06dfba8f301a244cce289d54877c36b4b19818e3b53150eb7c2424d0", size = 388983, upload-time = "2025-06-14T20:46:52.591Z" }, ] [[package]] name = "pyobjc-framework-collaboration" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/ee/1f6893eb882af5ecc6a6f4182b2ec85df777c4bc6b9a20a6b42c23abff3f/pyobjc_framework_collaboration-11.0.tar.gz", hash = "sha256:9f53929dd6d5b1a5511494432bf83807041c6f8b9ab6cf6ff184eee0b6f8226f", size = 17084, upload-time = "2025-01-14T19:03:01.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/49/9dbe8407d5dd663747267c1234d1b914bab66e1878d22f57926261a3063b/pyobjc_framework_collaboration-11.1.tar.gz", hash = "sha256:4564e3931bfc51773623d4f57f2431b58a39b75cb964ae5c48d27ee4dde2f4ea", size = 16839, upload-time = "2025-06-14T20:57:01.101Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ee/95883b6fbdbeecd99217c50c415ca024db5beb1923b935189a113412203d/pyobjc_framework_Collaboration-11.0-py2.py3-none-any.whl", hash = "sha256:acf11e584e21f6342e6d7be1675f36c92804082c29d2f373d1ca623a63959e76", size = 4807, upload-time = "2025-01-14T18:49:37.145Z" }, - { url = "https://files.pythonhosted.org/packages/c0/e5/d3ba7e3e3f306ba93c021c083287c668704d84605e0f788583abcfde815f/pyobjc_framework_Collaboration-11.0-py3-none-any.whl", hash = "sha256:e7789503ea9280ba365ce2c4e6c7c8b13dfa9174b2ecf9d174bbf9773f25f97a", size = 4876, upload-time = "2025-01-14T18:49:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/62/24/4c9deedcc62d223a45d4b4fa16162729923d2b3e2231467de6ecd079f3f8/pyobjc_framework_collaboration-11.1-py2.py3-none-any.whl", hash = "sha256:3629ea5b56c513fb330d43952afabb2df2a2ac2f9048b8ec6e8ab4486191390a", size = 4891, upload-time = "2025-06-14T20:46:59.734Z" }, ] [[package]] name = "pyobjc-framework-colorsync" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/24/397a80cd2313cc9e1b73b9acb1de66b740bbece4fe87ed4ea158de8fcef8/pyobjc_framework_colorsync-11.0.tar.gz", hash = "sha256:4f531f6075d9cc4b9d426620a1b04d3aaeb56b5ff178d0a6b0e93d068a5db0d2", size = 39249, upload-time = "2025-01-14T19:03:02.887Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/97/7613b6041f62c52f972e42dd5d79476b56b84d017a8b5e4add4d9cfaca36/pyobjc_framework_colorsync-11.1.tar.gz", hash = "sha256:7a346f71f34b2ccd1b020a34c219b85bf8b6f6e05283d503185aeb7767a269dd", size = 38999, upload-time = "2025-06-14T20:57:01.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/16/d806b5c3ff5bf8f46a4770f89b2076d2596c1301c851c60bb43aea457cd3/pyobjc_framework_ColorSync-11.0-py2.py3-none-any.whl", hash = "sha256:24f5c3e0987bfdfe6a0de36f2f908e30ea52000eb649db7b0373928140518163", size = 5916, upload-time = "2025-01-14T18:49:41.273Z" }, - { url = "https://files.pythonhosted.org/packages/06/18/777bad37aab42f75d2ef2efb9240308c15c33b3a0636278111ec6c5df550/pyobjc_framework_ColorSync-11.0-py3-none-any.whl", hash = "sha256:cbee2211f64be927eb4e4717bf6e275bf28954ed86e4a4655d367c30f856494d", size = 5987, upload-time = "2025-01-14T18:49:42.286Z" }, + { url = "https://files.pythonhosted.org/packages/30/d5/c8fc7c47cbb9865058094dc9cf3f57879156ff55fb261cf199e7081d1db7/pyobjc_framework_colorsync-11.1-py2.py3-none-any.whl", hash = "sha256:d19d6da2c7175a3896a63c9b40a8ab98ade0779a5b40062789681501c33efd5c", size = 5971, upload-time = "2025-06-14T20:47:00.547Z" }, ] [[package]] name = "pyobjc-framework-contacts" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/a2/89053853b28c1f2f2e69092d3e81b7c26073bc8396fc87772b3b1bfb9d57/pyobjc_framework_contacts-11.0.tar.gz", hash = "sha256:fc215baa9f66dbf9ffa1cb8170d102a3546cfd708b2b42de4e9d43645aec03d9", size = 84253, upload-time = "2025-01-14T19:03:03.743Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/85/34868b6447d552adf8674bac226b55c2baacacee0d67ee031e33805d6faa/pyobjc_framework_contacts-11.1.tar.gz", hash = "sha256:752036e7d8952a4122296d7772f274170a5f35a53ee6454a27f3e1d9603222cc", size = 84814, upload-time = "2025-06-14T20:57:02.582Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/4f/b7a7b08535015494940a62fd63825eccf4cace7f8ca87050f0837470eca8/pyobjc_framework_Contacts-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b16758fc1edc40f0ec288d67b7e39b59609fb1df2523f4362c958d150619dbe5", size = 11880, upload-time = "2025-01-14T18:49:44.316Z" }, - { url = "https://files.pythonhosted.org/packages/07/4b/0d2b41a32b6432182548cb84bb6b1c3228a7ff428ea15dfaf812b39c028f/pyobjc_framework_Contacts-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:80972851e2163b94d82fd4b0d9801790ad420dffa91a37c90fa2949031881c02", size = 11957, upload-time = "2025-01-14T18:49:46.952Z" }, + { url = "https://files.pythonhosted.org/packages/68/e1/27715ef476441cb05d4442b93fe6380a57a946cda008f70399cadb4ff1fd/pyobjc_framework_contacts-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:68148653f27c1eaeff2ad4831b5e68393071a382aab773629cd047ce55556726", size = 12067, upload-time = "2025-06-14T20:47:02.178Z" }, + { url = "https://files.pythonhosted.org/packages/30/c8/0d47af11112bf382e059cfe2dd03be98914f0621ddff8858bb9af864f8c5/pyobjc_framework_contacts-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:576ee4aec05d755444bff10b45833f73083b5b3d1b2740e133b92111f7765e54", size = 12141, upload-time = "2025-06-14T20:47:02.884Z" }, ] [[package]] name = "pyobjc-framework-contactsui" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-contacts" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/67/122b16fd7f2da7f0f48c1d7fcaf0f1951253ddd5489d909a1b5fb80f3925/pyobjc_framework_contactsui-11.0.tar.gz", hash = "sha256:d0f2a4afea807fbe4db1518c4f81f0dc9aa1817fe7cb16115308fc00375a70db", size = 19486, upload-time = "2025-01-14T19:03:04.72Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/57/8765b54a30edaa2a56df62e11e7c32e41b6ea300513256adffa191689368/pyobjc_framework_contactsui-11.1.tar.gz", hash = "sha256:5bc29ea2b10a342018e1b96be6b140c10ebe3cfb6417278770feef5e88026a1f", size = 20031, upload-time = "2025-06-14T20:57:03.603Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/47/b1dbe48c64e2d061bf8b4ee532413b97e6c5748fdba43598a30421086fcc/pyobjc_framework_ContactsUI-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd3efaf3f67e92704f41927c5de06ccc4aa9daa09865cba7ac476da9c6e1c3c2", size = 7734, upload-time = "2025-01-14T18:49:52.765Z" }, - { url = "https://files.pythonhosted.org/packages/5d/c5/465656c744301bfb7640e4077c57170d245843311e0e66702b53295e2534/pyobjc_framework_ContactsUI-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:da9c85dccdf518a0ac80c627daca32d56a4636e3f118359579de51a428e85ba7", size = 7739, upload-time = "2025-01-14T18:49:55.507Z" }, + { url = "https://files.pythonhosted.org/packages/38/02/f65f2eb6e2ad91c95e5a6b532fe8dd5cd0c190fbaff71e4a85346e16c0f6/pyobjc_framework_contactsui-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1c0f03c71e63daf5dbf760bf0e45620618a6f1ea62f8c17e288463c1fd4d2685", size = 7858, upload-time = "2025-06-14T20:47:08.346Z" }, + { url = "https://files.pythonhosted.org/packages/46/b6/50ec09f1bb18c422b8c079e02328689f32e977b43ab7651c05e8274854dc/pyobjc_framework_contactsui-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c34a6f27ef5aa4742cc44fd5b4d16fe1e1745ff839578b4c059faf2c58eee3ca", size = 7875, upload-time = "2025-06-14T20:47:09.041Z" }, ] [[package]] name = "pyobjc-framework-coreaudio" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/e6/3b7a8af3defec012d6cacf277fd8d5c3e254ceace63a05447dc1119f3a7e/pyobjc_framework_coreaudio-11.0.tar.gz", hash = "sha256:38b6b531381119be6998cf704d04c9ea475aaa33f6dd460e0584351475acd0ae", size = 140507, upload-time = "2025-01-14T19:03:05.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/c0/4ab6005cf97e534725b0c14b110d4864b367c282b1c5b0d8f42aad74a83f/pyobjc_framework_coreaudio-11.1.tar.gz", hash = "sha256:b7b89540ae7efc6c1e3208ac838ef2acfc4d2c506dd629d91f6b3b3120e55c1b", size = 141032, upload-time = "2025-06-14T20:57:04.348Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/f8/6f583376d2ef6a6123141d310f7f7e3e93ba9129ffbbc6eb26e25c4289c5/pyobjc_framework_CoreAudio-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:143cd44d5c069aee1abc5a88794e9531250b9fe70a98f6a08e493184dcf64b3e", size = 35750, upload-time = "2025-01-14T18:50:00.665Z" }, - { url = "https://files.pythonhosted.org/packages/df/14/b33556c06529a3c54853c41c5163e30a3fb9b2ae920e0c65a42ccd82e279/pyobjc_framework_CoreAudio-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d26eac5bc325bf046fc0bfdaa3322ddc828690dab726275f1c4c118bb888cc00", size = 36584, upload-time = "2025-01-14T18:50:01.806Z" }, + { url = "https://files.pythonhosted.org/packages/54/1d/81339c1087519a9f125396c717b85a05b49c2c54137bdf4ca01c1ccb6239/pyobjc_framework_coreaudio-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:73a46f0db2fa8ca2e8c47c3ddcc2751e67a0f8600246a6718553b15ee0dbbdb6", size = 35383, upload-time = "2025-06-14T20:47:14.234Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fe/c43521642db98a4ec29fa535781c1316342bb52d5fc709696cbb1e8ca6cd/pyobjc_framework_coreaudio-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2538d1242dab4e27efb346eafbad50594e7e95597fa7220f0bab2099c825da55", size = 36765, upload-time = "2025-06-14T20:47:15.344Z" }, ] [[package]] name = "pyobjc-framework-coreaudiokit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-coreaudio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/1a/604cac8d992b6e66adbb98edb1f65820116f5d74d8decd6d43898ae2929d/pyobjc_framework_coreaudiokit-11.0.tar.gz", hash = "sha256:1a4c3de4a02b0dfa7410c012c7f0939edd2e127d439fb934aeafc68450615f1d", size = 21450, upload-time = "2025-01-14T19:03:06.681Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/4e/c49b26c60047c511727efe994b412276c487dfe90f1ee0fced0bddbdf8a3/pyobjc_framework_coreaudiokit-11.1.tar.gz", hash = "sha256:0b461c3d6123fda4da6b6aaa022efc918c1de2e126a5cf07d2189d63fa54ba40", size = 21955, upload-time = "2025-06-14T20:57:05.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/b9/d75a4da2d6a3cb75bafd363c447d45e134fe78a340dee408423a40c04aac/pyobjc_framework_CoreAudioKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6dbf01f2625689b392c2ba02f3ab8186c914d84d6bd896bdef5181a15a9463df", size = 7192, upload-time = "2025-01-14T18:50:09.524Z" }, - { url = "https://files.pythonhosted.org/packages/46/1f/5c15023665cc0476cdd7cbc054d5b06489fc09990f068768ed2fda8a02a2/pyobjc_framework_CoreAudioKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ccf2d92052a446d1d38bfd7eaa1dcd2451d59c37e73070a9a1fee394a532d9d", size = 7214, upload-time = "2025-01-14T18:50:10.399Z" }, + { url = "https://files.pythonhosted.org/packages/07/44/0de5d26e383d0b00f2f44394db74e0953bc1e35b74072a67fde916e8e31e/pyobjc_framework_coreaudiokit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4743fbd210159cffffb0a7b8e06bf8b8527ba4bf01e76806fae2696fd6990e77", size = 7234, upload-time = "2025-06-14T20:47:21.271Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/d8ff6293851a7d9665724fa5c324d28200776ec10a04b850ba21ad1f9be1/pyobjc_framework_coreaudiokit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:20440a2926b1d91da8efc8bc060e77c7a195cb0443dbf3770eaca9e597276748", size = 7266, upload-time = "2025-06-14T20:47:22.136Z" }, ] [[package]] name = "pyobjc-framework-corebluetooth" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/74/66a62a36da9db5924ee15de6fe1eb544930609b307b3bfbc021b5cf43781/pyobjc_framework_corebluetooth-11.0.tar.gz", hash = "sha256:1dcb7c039c2efa7c72dc14cdda80e677240b49fa38999941a77ee02ca142998d", size = 59797, upload-time = "2025-01-14T19:03:07.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fe/2081dfd9413b7b4d719935c33762fbed9cce9dc06430f322d1e2c9dbcd91/pyobjc_framework_corebluetooth-11.1.tar.gz", hash = "sha256:1deba46e3fcaf5e1c314f4bbafb77d9fe49ec248c493ad00d8aff2df212d6190", size = 60337, upload-time = "2025-06-14T20:57:05.919Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/a8/df866e8a84fd33d29af1ee383f13715bbd98ad67d5795dfb276a3887560f/pyobjc_framework_CoreBluetooth-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:044069d63447554ba2c65cb1bf58d489d14ea279344810386392e583a2e611ef", size = 13683, upload-time = "2025-01-14T18:50:16.231Z" }, - { url = "https://files.pythonhosted.org/packages/44/fa/ad2165bc93c9d3fb174a0d8d5a4db3a7dfcf4dcaeca7913d59748ef62fdb/pyobjc_framework_CoreBluetooth-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bae8f909512d014eed85f80deae671185af4bb5a671fba19f85c7b4c973b61bb", size = 13713, upload-time = "2025-01-14T18:50:17.122Z" }, + { url = "https://files.pythonhosted.org/packages/8c/75/3318e85b7328c99c752e40592a907fc5c755cddc6d73beacbb432f6aa2d0/pyobjc_framework_corebluetooth-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:433b8593eb1ea8b6262b243ec903e1de4434b768ce103ebe15aac249b890cc2a", size = 13143, upload-time = "2025-06-14T20:47:28.889Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bc/083ea1ae57a31645df7fad59921528f6690995f7b7c84a203399ded7e7fe/pyobjc_framework_corebluetooth-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:36bef95a822c68b72f505cf909913affd61a15b56eeaeafea7302d35a82f4f05", size = 13163, upload-time = "2025-06-14T20:47:29.624Z" }, ] [[package]] name = "pyobjc-framework-coredata" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/22/6787205b91cb6d526b6b472ebaa5baff275200774050a55b4b25d2bd957a/pyobjc_framework_coredata-11.0.tar.gz", hash = "sha256:b11acb51ff31cfb69a53f4e127996bf194bcac770e8fa67cb5ba3fb16a496058", size = 260029, upload-time = "2025-01-14T19:03:08.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/e3/af497da7a7c895b6ff529d709d855a783f34afcc4b87ab57a1a2afb3f876/pyobjc_framework_coredata-11.1.tar.gz", hash = "sha256:fe9fd985f8e06c70c0fb1e6bbea5b731461f9e76f8f8d8e89c7c72667cdc6adf", size = 260628, upload-time = "2025-06-14T20:57:06.729Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/b0/32c23ee168e5081391daa8737fddde79670b083e948dffb8d74308f1dd43/pyobjc_framework_CoreData-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74ac5e7658df10544708f6017a8823a100fbe41dc3aa9f61bf2fd4f8773c3dd7", size = 16194, upload-time = "2025-01-14T18:50:22.924Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9e/39ca8124c6d87dc6fa85bcf850a2c23a062a408a26300062041c10363a3f/pyobjc_framework_CoreData-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c23b8c9106b0ec6f43aca80d2b2e0b0cc8fcb4ba78db4ae3c1f39a67464357d7", size = 16208, upload-time = "2025-01-14T18:50:23.838Z" }, + { url = "https://files.pythonhosted.org/packages/29/d9/7f12bdba0503d0ef7b1ac26e05429aecc33b4aaf190155a6bec8b576850d/pyobjc_framework_coredata-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c66ae04cc658eafdfb987f9705e21f9782edee6773a8adb6bfa190500e4e7e29", size = 16428, upload-time = "2025-06-14T20:47:34.981Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ac/77935aa9891bd6be952b1e6780df2bae748971dd0fe0b5155894004840bd/pyobjc_framework_coredata-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c9b2374784e67694a18fc8c120a12f11b355a20b643c01f23ae2ce87330a75e0", size = 16443, upload-time = "2025-06-14T20:47:35.711Z" }, ] [[package]] name = "pyobjc-framework-corehaptics" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/b8/66481497362171e7ad42fc8fcc0272c04b95a707c5c1e7e8f8a8bfe58917/pyobjc_framework_corehaptics-11.0.tar.gz", hash = "sha256:1949b56ac0bd4219eb04c466cdd0f7f93d6826ed92ee61f01a4b5e98139ee039", size = 42956, upload-time = "2025-01-14T19:03:09.753Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/83/cc997ec4687a68214dd3ad1bdf64353305f5c7e827fad211adac4c28b39f/pyobjc_framework_corehaptics-11.1.tar.gz", hash = "sha256:e5da3a97ed6aca9b7268c8c5196c0a339773a50baa72d1502d3435dc1a2a80f1", size = 42722, upload-time = "2025-06-14T20:57:08.019Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/16/16d4365c8da1f708e145500237a3cdbbdde3e83b7f3f8673b038efac03b9/pyobjc_framework_CoreHaptics-11.0-py2.py3-none-any.whl", hash = "sha256:ff1d8f58dd3b29287dfad16a60bb45706c91f1910e400b632cb664eb2e56588b", size = 5307, upload-time = "2025-01-14T18:50:33.074Z" }, - { url = "https://files.pythonhosted.org/packages/12/72/b9fca92b3704af8f5f3b5507d0d9f3d0f5eb16605664de669f4468858627/pyobjc_framework_CoreHaptics-11.0-py3-none-any.whl", hash = "sha256:33f7a767efe6867fa6821ad73872ea88aec44650a22217bcdc9c1ec7c41fd9dc", size = 5377, upload-time = "2025-01-14T18:50:34.484Z" }, + { url = "https://files.pythonhosted.org/packages/21/d0/0fb20c0f19beae53c905653ffdcbf32e3b4119420c737ff4733f7ebb3b29/pyobjc_framework_corehaptics-11.1-py2.py3-none-any.whl", hash = "sha256:8f8c47ccca5052d07f95d2f35e6e399c5ac1f2072ba9d9eaae902edf4e3a7af4", size = 5363, upload-time = "2025-06-14T20:47:40.582Z" }, ] [[package]] name = "pyobjc-framework-corelocation" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/2d/b21ca49a34db49390420a9d7d05fd9eb89850dbec0a555c9ee408f52609c/pyobjc_framework_corelocation-11.0.tar.gz", hash = "sha256:05055c3b567f7f8f796845da43fb755d84d630909b927a39f25cf706ef52687d", size = 103955, upload-time = "2025-01-14T19:03:10.707Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/ef/fbd2e01ec137208af7bfefe222773748d27f16f845b0efa950d65e2bd719/pyobjc_framework_corelocation-11.1.tar.gz", hash = "sha256:46a67b99925ee3d53914331759c6ee110b31bb790b74b05915acfca41074c206", size = 104508, upload-time = "2025-06-14T20:57:08.731Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/99/c7844f6e583f4764c6fab4a5b5ad9e949c6fce8c30f95226118bead41e01/pyobjc_framework_CoreLocation-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:046f211a23de55364c8553cfd660dc5adeff28af4d25f5ed9b5a8bfa83266b4d", size = 13075, upload-time = "2025-01-14T18:50:37.789Z" }, - { url = "https://files.pythonhosted.org/packages/88/6b/bb4fbcd259404fb60fdbfecef3c426dc23da5a0f0bc5bf96a4169b047478/pyobjc_framework_CoreLocation-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9bca9974f143bc9e93bd7ec4ef91655964d8ad0ca5ffccc8404fb6f098fa08dc", size = 13076, upload-time = "2025-01-14T18:50:38.693Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f9/8137e8bf86f8e6350298217dcc635fd6577d64b484f9d250ddb85a84efa0/pyobjc_framework_corelocation-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ea261e7d87c6f62f1b03c252c273ea7fd6f314e3e73c69c6fb3fe807bf183462", size = 12741, upload-time = "2025-06-14T20:47:42.583Z" }, + { url = "https://files.pythonhosted.org/packages/95/cb/282d59421cdb89a5e5fcce72fc37d6eeace98a2a86d71f3be3cd47801298/pyobjc_framework_corelocation-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:562e31124f80207becfd8df01868f73fa5aa70169cc4460e1209fb16916e4fb4", size = 12752, upload-time = "2025-06-14T20:47:43.273Z" }, ] [[package]] name = "pyobjc-framework-coremedia" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/60/7c7b9f13c94910882de6cc08f48a52cce9739e75cc3b3b6de5c857e6536a/pyobjc_framework_coremedia-11.0.tar.gz", hash = "sha256:a414db97ba30b43c9dd96213459d6efb169f9e92ce1ad7a75516a679b181ddfb", size = 249161, upload-time = "2025-01-14T19:03:12.291Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/5d/81513acd219df77a89176f1574d936b81ad6f6002225cabb64d55efb7e8d/pyobjc_framework_coremedia-11.1.tar.gz", hash = "sha256:82cdc087f61e21b761e677ea618a575d4c0dbe00e98230bf9cea540cff931db3", size = 216389, upload-time = "2025-06-14T20:57:09.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/b3/7baca352ddd7256840a4eb8f38fda39bc2e023b222b86d11c1a77cc0a8fa/pyobjc_framework_CoreMedia-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:057e63e533577fe5d764a5a9d307f60e8d9c58803112951ace42183abe9437e3", size = 29422, upload-time = "2025-01-14T18:50:56.964Z" }, - { url = "https://files.pythonhosted.org/packages/68/73/7ed3eba9c5a4a2071c3a64d6b1388d13474ad8d972529f3d5c950942513d/pyobjc_framework_CoreMedia-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:afd8eb59f5ce0730ff15476ad3989aa84ffb8d8d02c9b8b2c9c1248b0541dbff", size = 29297, upload-time = "2025-01-14T18:50:58.388Z" }, + { url = "https://files.pythonhosted.org/packages/32/48/811ccea77d2c0d8156a489e2900298502eb6648d9c041c7f0c514c8f8a29/pyobjc_framework_coremedia-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aacf47006e1c6bf6124fb2b5016a8d5fd5cf504b6b488f9eba4e389ab0f0a051", size = 29118, upload-time = "2025-06-14T20:47:48.895Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b3d004d6a2d2188d196779d92fe8cfa2533f5722cd216fbc4f0cffc63b24/pyobjc_framework_coremedia-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ea5055298af91e463ffa7977d573530f9bada57b8f2968dcc76a75e339b9a598", size = 29015, upload-time = "2025-06-14T20:47:49.655Z" }, ] [[package]] name = "pyobjc-framework-coremediaio" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/59/904af57d302caa4c20d3bfebb9fb9300ccc3c396134460821c9f1e8ab65b/pyobjc_framework_coremediaio-11.0.tar.gz", hash = "sha256:7d652cf1a2a75c78ea6e8dbc7fc8b782bfc0f07eafc84b700598172c82f373d8", size = 107856, upload-time = "2025-01-14T19:03:14.225Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/68/9cef2aefba8e69916049ff43120e8794df8051bdf1f690a55994bbe4eb57/pyobjc_framework_coremediaio-11.1.tar.gz", hash = "sha256:bccd69712578b177144ded398f4695d71a765ef61204da51a21f0c90b4ad4c64", size = 108326, upload-time = "2025-06-14T20:57:10.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/02/09fda96c4727ff0c632c3cf4b09faa5b82be9f18422860dd80b5bc676ae1/pyobjc_framework_CoreMediaIO-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4182b91c72923d5c4d52eca3c221cc6f149d80a96242c0caab1d5bc9ccbcbbb", size = 17492, upload-time = "2025-01-14T18:51:04.559Z" }, - { url = "https://files.pythonhosted.org/packages/3f/db/a7b11cbf7d31964a65c5593ac30a02b0db35260845431046d467b08fc059/pyobjc_framework_CoreMediaIO-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1ad1e0f74126b6c6d25017e4ba08f66fe5422c902060d64b69e06a0c10214355", size = 17534, upload-time = "2025-01-14T18:51:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/6bcddd7d57fa19173621aa29b46342756ed1a081103d24e4bdac1cf882fe/pyobjc_framework_coremediaio-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4438713ee4611d5310f4f2e71e557b6138bc79c0363e3d45ecb8c09227dfa58e", size = 17203, upload-time = "2025-06-14T20:47:55.781Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b5/5dd941c1d7020a78b563a213fb8be7c6c3c1073c488914e158cd5417f4f7/pyobjc_framework_coremediaio-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39ad2518de9943c474e5ca0037e78f92423c3352deeee6c513a489016dac1266", size = 17250, upload-time = "2025-06-14T20:47:56.505Z" }, ] [[package]] name = "pyobjc-framework-coremidi" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/90/d004cdf4c52b8b16842e15135495de882d743b4f0217946bd8ae1a920173/pyobjc_framework_coremidi-11.0.tar.gz", hash = "sha256:acace4448b3e4802ab5dd75bbf875aae5e1f6c8cab2b2f1d58af20fc8b2a5a7f", size = 107342, upload-time = "2025-01-14T19:03:15.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ca/2ae5149966ccd78290444f88fa62022e2b96ed2fddd47e71d9fd249a9f82/pyobjc_framework_coremidi-11.1.tar.gz", hash = "sha256:095030c59d50c23aa53608777102bc88744ff8b10dfb57afe24b428dcd12e376", size = 107817, upload-time = "2025-06-14T20:57:11.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/b9/c67886891ad3cd21107cf1e65f1431cbdcff33acd74bf55ad3d6e10f3adf/pyobjc_framework_CoreMIDI-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:78dec1bcd253a0385ac0b758a455e2a9367fc3cb9e2306d410c61bafa8d4c2eb", size = 24314, upload-time = "2025-01-14T18:50:44.817Z" }, - { url = "https://files.pythonhosted.org/packages/c0/7a/0639bc1ac35373b68f0f15fbcb9bb4f317cc4452997ea8e611ce79f623e9/pyobjc_framework_CoreMIDI-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:97158830d76b999255d87191f31624d4373ee8ff662af4f4376c584cfb805573", size = 24346, upload-time = "2025-01-14T18:50:45.805Z" }, + { url = "https://files.pythonhosted.org/packages/37/fc/db75c55e492e5e34be637da2eeeaadbb579655b6d17159de419237bc9bdf/pyobjc_framework_coremidi-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5f8c2fdc9d1b7967e2a5ec0d5281eaddc00477bed9753aa14d5b881dc3a9ad8f", size = 24256, upload-time = "2025-06-14T20:48:02.448Z" }, + { url = "https://files.pythonhosted.org/packages/c2/2d/57c279dd74a9073d1416b10b05ebb9598f4868cad010d87f46ef4b517324/pyobjc_framework_coremidi-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:deb9120478a831a898f22f68737fc683bb9b937e77556e78b75986aebd349c55", size = 24277, upload-time = "2025-06-14T20:48:03.184Z" }, ] [[package]] name = "pyobjc-framework-coreml" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/64/4f0a990ec0955fe9b88f1fa58303c8471c551996670216527b4ac559ed8f/pyobjc_framework_coreml-11.0.tar.gz", hash = "sha256:143a1f73a0ea0a0ea103f3175cb87a61bbcb98f70f85320ed4c61302b9156d58", size = 81452, upload-time = "2025-01-14T19:03:16.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5d/4309f220981d769b1a2f0dcb2c5c104490d31389a8ebea67e5595ce1cb74/pyobjc_framework_coreml-11.1.tar.gz", hash = "sha256:775923eefb9eac2e389c0821b10564372de8057cea89f1ea1cdaf04996c970a7", size = 82005, upload-time = "2025-06-14T20:57:12.004Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/dc/334823bb3faa490259df7611772e804eb883c56436fc69123e8a3a5ba0ea/pyobjc_framework_CoreML-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e290ad9c0ac5f057ce3885d35e33fadc115f59111f2e04f168c45e2890cb86e8", size = 11320, upload-time = "2025-01-14T18:50:50.914Z" }, - { url = "https://files.pythonhosted.org/packages/90/9f/3d053b95fbeeaf480d33fcc067504e205049591f6bee17e3a700b988d96c/pyobjc_framework_CoreML-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:48320a57589634c206d659799284a5133aaa006cf4562f772697df5b479043e4", size = 11321, upload-time = "2025-01-14T18:50:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/2218a8f457f56075a8a3f2490bd9d01c8e69c807139eaa0a6ac570531ca5/pyobjc_framework_coreml-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b5be7889ad99da1aca040238fd99af9ee87ea8a6628f24d33e2e4890b88dd139", size = 11414, upload-time = "2025-06-14T20:48:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/3e/9e/a1b6d30b4f91c7cc4780e745e1e73a322bd3524a773f66f5eac0b1600d85/pyobjc_framework_coreml-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c768b03d72488b964d753392e9c587684961d8237b69cca848b3a5a00aea79c9", size = 11436, upload-time = "2025-06-14T20:48:10.048Z" }, ] [[package]] name = "pyobjc-framework-coremotion" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/79/5c4ff39a48f0dc0f764d1330b2360e9f31e3a32414e8690e7f20e4574e93/pyobjc_framework_coremotion-11.0.tar.gz", hash = "sha256:d1e7ca418897e35365d07c6fd5b5d625a3c44261b6ce46dcf80787f634ad6fa5", size = 66508, upload-time = "2025-01-14T19:03:17.254Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/95/e469dc7100ea6b9c29a074a4f713d78b32a78d7ec5498c25c83a56744fc2/pyobjc_framework_coremotion-11.1.tar.gz", hash = "sha256:5884a568521c0836fac39d46683a4dea3d259a23837920897042ffb922d9ac3e", size = 67050, upload-time = "2025-06-14T20:57:12.705Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/35/da29fd7350cd68bfe70f30a9e03e1350d7363c7c4fcdb5b0cd16f4bb47e2/pyobjc_framework_CoreMotion-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8e32de44e30028500e4d17c114eea69e7e74e5ae7aef6c208edc5bac34dfc21e", size = 10229, upload-time = "2025-01-14T18:51:12.825Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f6/8061b58f0f3e1daf34c19511f0eeefe4ad66d10d1994b84d7fa3733b7852/pyobjc_framework_CoreMotion-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:697a3121615e95e56808f388b0882217a50e5ff6b459eccae93f2809d5ea4389", size = 10250, upload-time = "2025-01-14T18:51:13.746Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3f/137c983dbccbdbc4a07fb2453e494af938078969bcde9252fbbad0ba939d/pyobjc_framework_coremotion-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:501248a726816e05552d1c1f7e2be2c7305cda792c46905d9aee7079dfad2eea", size = 10353, upload-time = "2025-06-14T20:48:15.365Z" }, + { url = "https://files.pythonhosted.org/packages/e9/17/ffa3cf9fde9df31f3d6ecb38507c61c6d8d81276d0a9116979cafd5a0ab7/pyobjc_framework_coremotion-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8c3b33228a170bf8495508a8923451ec600435c7bff93d7614f19c913baeafd1", size = 10368, upload-time = "2025-06-14T20:48:16.066Z" }, ] [[package]] name = "pyobjc-framework-coreservices" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-fsevents" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/b5/19c096b9938d6e2fdb1b436f21ad989b77dbeb4e59b3db4bd344800fa1e8/pyobjc_framework_coreservices-11.0.tar.gz", hash = "sha256:ac96954f1945a1153bdfef685611665749eaa8016b5af6f34bd56a274952b03a", size = 1244406, upload-time = "2025-01-14T19:03:19.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/a9/141d18019a25776f507992f9e7ffc051ca5a734848d8ea8d848f7c938efc/pyobjc_framework_coreservices-11.1.tar.gz", hash = "sha256:cf8eb5e272c60a96d025313eca26ff2487dcd02c47034cc9db39f6852d077873", size = 1245086, upload-time = "2025-06-14T20:57:13.914Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/cc/3899a59ed62fa36d2c1b95b94ff52e181ac48fde4011b68ca6abcbddd47a/pyobjc_framework_CoreServices-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7538ca6e22f4da0c3a70ddd9781f9240a3fe2fd7a7aa4dfb31c31f2532f008e", size = 30258, upload-time = "2025-01-14T18:51:20.487Z" }, - { url = "https://files.pythonhosted.org/packages/82/7b/8e059764951d0414f053bfebb6b1fba803a3b14397755cfd388b0a6363a7/pyobjc_framework_CoreServices-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3b175b5aa7a78484fd07b93533174b125901a6b791df2c51e05df1ea5d5badab", size = 30250, upload-time = "2025-01-14T18:51:21.448Z" }, + { url = "https://files.pythonhosted.org/packages/fc/35/a984b9aace173e92b3509f82afe5e0f8ecddf5cf43bf0c01c803f60a19ce/pyobjc_framework_coreservices-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7260e09a0550d57756ad655f3d3815f21fc3f0386aed014be4b46194c346941", size = 30243, upload-time = "2025-06-14T20:48:21.563Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0f/52827197a1fa1dabefd77803920eaf340f25e0c81944844ab329d511cade/pyobjc_framework_coreservices-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6bd313ec326efd715b4b10c3ebcc9f054e3ee3178be407b97ea225cd871351d2", size = 30252, upload-time = "2025-06-14T20:48:22.657Z" }, ] [[package]] name = "pyobjc-framework-corespotlight" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/6a/6707d7ef339b9ad2dd0994d1df42969ee3b231f2d098f3377d40aed60b4f/pyobjc_framework_corespotlight-11.0.tar.gz", hash = "sha256:a96c9b4ba473bc3ee19afa01a9af989458e6a56e9656c2cdea1850d2b13720e6", size = 86130, upload-time = "2025-01-14T19:03:20.457Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/b67ebfb63b7ccbfda780d583056d1fd4b610ba3839c8ebe3435b86122c61/pyobjc_framework_corespotlight-11.1.tar.gz", hash = "sha256:4dd363c8d3ff7619659b63dd31400f135b03e32435b5d151459ecdacea14e0f2", size = 87161, upload-time = "2025-06-14T20:57:14.934Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/f1/54f9522d7f6ec7a6618c86abe0236869f61dd371b49df02dff7930433656/pyobjc_framework_CoreSpotlight-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:551878bfb9cc815fe2532fdf455f500dda44f8cd203dd836a6f1eb5cc0d49a9a", size = 9579, upload-time = "2025-01-14T18:51:28.889Z" }, - { url = "https://files.pythonhosted.org/packages/6c/24/dae8d0be7cb90328a8c1100c454e52faef95acc59940796f530b665b9555/pyobjc_framework_CoreSpotlight-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b0c595d0a422a0f81008df93a0a2b38a1fd62434c6f61e31f1dceec927803b80", size = 9597, upload-time = "2025-01-14T18:51:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/46/d4/87a87384bbb2e27864d527eb00973a056bae72603e6c581711231f2479fc/pyobjc_framework_corespotlight-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3c571289ce9107f1ade92ad036633f81355f22f70e8ba82d7335f1757381b89", size = 9954, upload-time = "2025-06-14T20:48:28.065Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f8/06b7edfeabe5b3874485b6e5bbe4a39d9f2e1f44348faa7cb320fbc6f21a/pyobjc_framework_corespotlight-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7cedd3792fe1fe2a8dc65a8ff1f70baf12415a5dc9dc4d88f987059567d7e694", size = 9977, upload-time = "2025-06-14T20:48:28.757Z" }, ] [[package]] name = "pyobjc-framework-coretext" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/e8/9b68dc788828e38143a3e834e66346713751cb83d7f0955016323005c1a2/pyobjc_framework_coretext-11.0.tar.gz", hash = "sha256:a68437153e627847e3898754dd3f13ae0cb852246b016a91f9c9cbccb9f91a43", size = 274222, upload-time = "2025-01-14T19:03:21.521Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/e9/d3231c4f87d07b8525401fd6ad3c56607c9e512c5490f0a7a6abb13acab6/pyobjc_framework_coretext-11.1.tar.gz", hash = "sha256:a29bbd5d85c77f46a8ee81d381b847244c88a3a5a96ac22f509027ceceaffaf6", size = 274702, upload-time = "2025-06-14T20:57:16.059Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/20/b8a967101b585a2425ffe645135f8618edd51e1430aeb668373475a07d1f/pyobjc_framework_CoreText-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:56a4889858308b0d9f147d568b4d91c441cc0ffd332497cb4f709bb1990450c1", size = 30397, upload-time = "2025-01-14T18:51:35.844Z" }, - { url = "https://files.pythonhosted.org/packages/0d/14/d300b8bf18acd1d98d40820d2a9b5c5b6cf96325bdfc5020bc963218e001/pyobjc_framework_CoreText-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb90e7f370b3fd7cb2fb442e3dc63fedf0b4af6908db1c18df694d10dc94669d", size = 30456, upload-time = "2025-01-14T18:51:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/d6cc5470157cfd328b2d1ee2c1b6f846a5205307fce17291b57236d9f46e/pyobjc_framework_coretext-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4f4d2d2a6331fa64465247358d7aafce98e4fb654b99301a490627a073d021e", size = 30072, upload-time = "2025-06-14T20:48:34.248Z" }, + { url = "https://files.pythonhosted.org/packages/32/67/9cc5189c366e67dc3e5b5976fac73cc6405841095f795d3fa0d5fc43d76a/pyobjc_framework_coretext-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1597bf7234270ee1b9963bf112e9061050d5fb8e1384b3f50c11bde2fe2b1570", size = 30175, upload-time = "2025-06-14T20:48:35.023Z" }, ] [[package]] name = "pyobjc-framework-corewlan" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/a9/cda522b270adb75d62bae447b2131da62912b5eda058a07e3a433689116f/pyobjc_framework_corewlan-11.0.tar.gz", hash = "sha256:8803981d64e3eb4fa0ea56657a9b98e4004de5a84d56e32e5444815d8ed6fa6f", size = 65254, upload-time = "2025-01-14T19:03:23.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/d8/03aff3c75485fc999e260946ef1e9adf17640a6e08d7bf603d31cfcf73fc/pyobjc_framework_corewlan-11.1.tar.gz", hash = "sha256:4a8afea75393cc0a6fe696e136233aa0ed54266f35a47b55a3583f4cb078e6ce", size = 65792, upload-time = "2025-06-14T20:57:16.931Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/e7/a869bf3e8673c8fdf496706672dac77fc305493db3c1057e3ca5f8d49c3f/pyobjc_framework_CoreWLAN-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4384ba68d4beb4d610ca0d661593e16efe541faf1790222b898b3f4dd389c98a", size = 9895, upload-time = "2025-01-14T18:51:43.245Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d7/87626e23f010aa865eef10c796d1d87ddd87b78656f4e4ef0e808c8268f7/pyobjc_framework_CoreWLAN-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5f5c365f6ebdae4a87d534cf8af877a57d2aabe50fe5949a9334e75173291898", size = 9917, upload-time = "2025-01-14T18:51:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/e73152fc1beee1bf75489d4a6f89ebd9783340e50ca1948cde029d7b0411/pyobjc_framework_corewlan-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e12f127b37a7ab8f349167332633392f2d6d29b87c9b98137a289d0fc1e07b5b", size = 9993, upload-time = "2025-06-14T20:48:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/09/8a/74feabaad1225eb2c44d043924ed8caea31683e6760cd9b918b8d965efea/pyobjc_framework_corewlan-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7bd0775d2466ad500aad4747d8a889993db3a14240239f30ef53c087745e9c8e", size = 10016, upload-time = "2025-06-14T20:48:41.792Z" }, ] [[package]] name = "pyobjc-framework-cryptotokenkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/72/b871fa5476479e4a22a4a0e971fb4724b0eb94c721365539ad55f4dc3135/pyobjc_framework_cryptotokenkit-11.0.tar.gz", hash = "sha256:a1bbfe9170c35cb427d39167af55aefea651c5c8a45c0de60226dae04b61a6b1", size = 58734, upload-time = "2025-01-14T19:03:24.851Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/92/7fab6fcc6bb659d6946cfb2f670058180bcc4ca1626878b0f7c95107abf0/pyobjc_framework_cryptotokenkit-11.1.tar.gz", hash = "sha256:5f82f44d9ab466c715a7c8ad4d5ec47c68aacd78bd67b5466a7b8215a2265328", size = 59223, upload-time = "2025-06-14T20:57:17.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/60/ddf022ce94f829a605992f11b9bfa861d7a1579f794e03d969c209d0de2a/pyobjc_framework_CryptoTokenKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3c42620047cc75a749fbed045d181dc76284bc27edea904b97df1ad82c2fdafc", size = 12949, upload-time = "2025-01-14T18:51:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2d/9641cae1800281faace48698646f71c3de23ea1343031c12f6637d31e6f1/pyobjc_framework_CryptoTokenKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:95b05efb06b09987e23fb62dc3af378f38cfd0bd5872940cd95cf0f39dac6a57", size = 12978, upload-time = "2025-01-14T18:51:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b6/783495dc440277a330930bac7b560cf54d5e1838fc30fdc3162722db8a62/pyobjc_framework_cryptotokenkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b76fb928bc398091141dc52b26e02511065afd0b6de5533fa0e71ab13c51589", size = 12515, upload-time = "2025-06-14T20:48:47.346Z" }, + { url = "https://files.pythonhosted.org/packages/76/f1/4cb9c90a55ec13301d60ac1c4d774c37b4ebc6db6331d3853021c933fcc8/pyobjc_framework_cryptotokenkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6384cb1d86fc586e2da934a5a37900825bd789e3a5df97517691de9af354af0c", size = 12543, upload-time = "2025-06-14T20:48:48.079Z" }, ] [[package]] name = "pyobjc-framework-datadetection" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/6b/b896feb16e914dc81b6ed6cdbd0b6e6390eaafc80fff5297ec17eb0bd716/pyobjc_framework_datadetection-11.0.tar.gz", hash = "sha256:9967555151892f8400cffac86e8656f2cb8d7866963fdee255e0747fa1386533", size = 13738, upload-time = "2025-01-14T19:03:27.054Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/4d/65c61d8878b44689e28d5729be9edbb73e20b1b0500d1095172cfd24aea6/pyobjc_framework_datadetection-11.1.tar.gz", hash = "sha256:cbe0080b51e09b2f91eaf2a9babec3dcf2883d7966bc0abd8393ef7abfcfc5db", size = 13485, upload-time = "2025-06-14T20:57:18.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/a1/63653827a78c8329a0106ac06e68ec0434e7f104f022dee5929bdf8fed62/pyobjc_framework_DataDetection-11.0-py2.py3-none-any.whl", hash = "sha256:0fd191ddee9bc6a491e05dfb7de780c0266fd6c90ca783e168786c4b0b5d7d7c", size = 3428, upload-time = "2025-01-14T18:51:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/1b/61/ee4579efb7c02b794d26ab0458722598726678d0bb227c9aa925a34f36af/pyobjc_framework_DataDetection-11.0-py3-none-any.whl", hash = "sha256:21b4a1dbf6cb56fdc971224476453dd1a7a4bb72d2c670444e81ae96fde97cb2", size = 3501, upload-time = "2025-01-14T18:51:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/08/c4/ef2136e4e0cc69b02479295822aa33c8e26995b265c8a1184867b65a0a06/pyobjc_framework_datadetection-11.1-py2.py3-none-any.whl", hash = "sha256:5afd3dde7bba3324befb7a3133c9aeaa5088efd72dccc0804267a74799f4a12f", size = 3482, upload-time = "2025-06-14T20:48:54.301Z" }, ] [[package]] name = "pyobjc-framework-devicecheck" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/f8/237a92dd9ba8a88b7027f78cba83e61b0011bfc2a49351ecaa177233f639/pyobjc_framework_devicecheck-11.0.tar.gz", hash = "sha256:66cff0323dc8eef1b76d60f9c9752684f11e534ebda60ecbf6858a9c73553f64", size = 14198, upload-time = "2025-01-14T19:03:27.918Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/f2/b1d263f8231f815a9eeff15809f4b7428dacdc0a6aa267db5ed907445066/pyobjc_framework_devicecheck-11.1.tar.gz", hash = "sha256:8b05973eb2673571144d81346336e749a21cec90bd7fcaade76ffd3b147a0741", size = 13954, upload-time = "2025-06-14T20:57:19.782Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/c1/d889e1c515c23b911594aa0b53a9d8ab6173e07adaaad8db89324a731fb7/pyobjc_framework_DeviceCheck-11.0-py2.py3-none-any.whl", hash = "sha256:d9252173a57dfba09ae37ccc3049f4b4990c1cbdcde338622b42c66296a8740e", size = 3612, upload-time = "2025-01-14T18:52:00.097Z" }, - { url = "https://files.pythonhosted.org/packages/65/8b/fa0cc2da2d49897f64e27a8a4e2a68f5784515f1adcea3a90f90b8ae8d44/pyobjc_framework_DeviceCheck-11.0-py3-none-any.whl", hash = "sha256:e8ed3965808963b2f0a7e069537d752bc659b75db1901cc24e5138925b9a7052", size = 3684, upload-time = "2025-01-14T18:52:02.389Z" }, + { url = "https://files.pythonhosted.org/packages/39/72/17698a0d68b1067b20b32b4afd74bcafb53a7c73ae8fc608addc7b9e7a37/pyobjc_framework_devicecheck-11.1-py2.py3-none-any.whl", hash = "sha256:8edb36329cdd5d55e2c2c57c379cb5ba1f500f74a08fe8d2612b1a69b7a26435", size = 3668, upload-time = "2025-06-14T20:48:55.098Z" }, ] [[package]] name = "pyobjc-framework-devicediscoveryextension" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/48/178a1879109128f34334fdae2fe4463c7620f169593bea96704f347d945e/pyobjc_framework_devicediscoveryextension-11.0.tar.gz", hash = "sha256:576dac3f418cfc4f71020a45f06231d14e4b2a8e182ef0020dd9da3cf238d02f", size = 14511, upload-time = "2025-01-14T19:03:32.132Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/b8/102863bfa2f1e414c88bb9f51151a9a58b99c268a841b59d46e0dcc5fe6d/pyobjc_framework_devicediscoveryextension-11.1.tar.gz", hash = "sha256:ae160ea40f25d3ee5e7ce80ac9c1b315f94d0a4c7ccb86920396f71c6bf799a0", size = 14298, upload-time = "2025-06-14T20:57:20.738Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/be/3353a87691796a277ff4c048c4fa9a43db6f353fd683e8bb9e297651950c/pyobjc_framework_DeviceDiscoveryExtension-11.0-py2.py3-none-any.whl", hash = "sha256:82032e567d0031839d626947368d6d3d4ca97c915f15d2779a444cf4b2ffa4a3", size = 4194, upload-time = "2025-01-14T18:52:03.253Z" }, - { url = "https://files.pythonhosted.org/packages/06/87/52137a60498c03ab0acd3b9eadafe3c371c12e0549718e6a1f0fff8b7725/pyobjc_framework_DeviceDiscoveryExtension-11.0-py3-none-any.whl", hash = "sha256:9c94057173f13472089d561b780d93b5aa244d048b4760a0e1ab54fe7c2253c5", size = 4265, upload-time = "2025-01-14T18:52:05.101Z" }, + { url = "https://files.pythonhosted.org/packages/67/89/fce0c0c89746f399d13e08b40fc12e29a2495f4dcebd30893336d047af18/pyobjc_framework_devicediscoveryextension-11.1-py2.py3-none-any.whl", hash = "sha256:96e5b13c718bd0e6c80fbd4e14b8073cffc88b3ab9bb1bbb4dab7893a62e4f11", size = 4249, upload-time = "2025-06-14T20:48:55.895Z" }, ] [[package]] name = "pyobjc-framework-dictionaryservices" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-coreservices" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/cf/2913c7df737eb8519acb7ef6429127e40d6c334415e38cfa18d6481150eb/pyobjc_framework_dictionaryservices-11.0.tar.gz", hash = "sha256:6b5f27c75424860f169e7c7e182fabffdba22854fedb8023de180e8770661dce", size = 10823, upload-time = "2025-01-14T19:03:32.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/13/c46f6db61133fee15e3471f33a679da2af10d63fa2b4369e0cd476988721/pyobjc_framework_dictionaryservices-11.1.tar.gz", hash = "sha256:39c24452d0ddd037afeb73a1742614c94535f15b1c024a8a6cc7ff081e1d22e7", size = 10578, upload-time = "2025-06-14T20:57:21.392Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/68/5ea9766a8a6301f1a2ee39d595fe03d50b84b979d3d059e3e0ff541eab45/pyobjc_framework_DictionaryServices-11.0-py2.py3-none-any.whl", hash = "sha256:7c081371855240ac8e22783a71f32393c0f1e0b94d2fd193e8fef0a8be007080", size = 3829, upload-time = "2025-01-14T18:52:07.379Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c4/62b73f813c012f72a3a8e2f6326506803b45e91dc4ce6683e02a52a7f414/pyobjc_framework_DictionaryServices-11.0-py3-none-any.whl", hash = "sha256:15cdc3b64cb73713ee928cdcc0a12c845729f117bb8e73c7511f6e3f256d9d39", size = 3901, upload-time = "2025-01-14T18:52:08.403Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/4e757b4064a0feb8d60456672560adad0bb5df530ba6621fe65d175dbd90/pyobjc_framework_dictionaryservices-11.1-py2.py3-none-any.whl", hash = "sha256:92f4871066653f18e2394ac93b0a2ab50588d60020f6b3bd93e97b67cd511326", size = 3913, upload-time = "2025-06-14T20:48:56.806Z" }, ] [[package]] name = "pyobjc-framework-discrecording" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/cc/f36612b67ca1fff7659d7933b563dce61f8c84dad0bf79fab08bb34949ad/pyobjc_framework_discrecording-11.0.tar.gz", hash = "sha256:6bdc533f067d049ea5032f65af70b5cdab68673574ac32dacb46509a9411d256", size = 122426, upload-time = "2025-01-14T19:03:35.589Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/b2/d8d1a28643c2ab681b517647bacb68496c98886336ffbd274f0b2ad28cdc/pyobjc_framework_discrecording-11.1.tar.gz", hash = "sha256:37585458e363b20bb28acdb5cc265dfca934d8a07b7baed2584953c11c927a87", size = 123004, upload-time = "2025-06-14T20:57:22.01Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/0b/fbe460ccddb4c613eb04e2b81cc9c75b0e0c407fd9fb91776381416f99af/pyobjc_framework_DiscRecording-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eab79d83c2d974aa5564f3f6f4415218573dca69010026d2d000d232494a9d81", size = 14491, upload-time = "2025-01-14T18:52:10.415Z" }, - { url = "https://files.pythonhosted.org/packages/10/6f/c4c220d979771f4d7782deddef5ea9026baa177abe81cbe63d626a215de7/pyobjc_framework_DiscRecording-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e309e7394aed23d6ccce2e035f23c0c015d029c2ad531c6b1dce820b7eea8512", size = 14505, upload-time = "2025-01-14T18:52:11.414Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8c/0ff85cc34218e54236eb866e71c35e3308a661f50aea090d400e9121d9c4/pyobjc_framework_discrecording-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dc8a7820fc193c2bfcd843c31de945dc45e77e5413089eabbc72be16a4f52e53", size = 14558, upload-time = "2025-06-14T20:48:58.495Z" }, + { url = "https://files.pythonhosted.org/packages/5e/17/032fa44bb66b6a20c432f3311072f88478b42dcf39b21ebb6c3bbdf2954f/pyobjc_framework_discrecording-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e29bc8c3741ae52fae092f892de856dbab2363e71537a8ae6fd026ecb88e2252", size = 14581, upload-time = "2025-06-14T20:48:59.228Z" }, ] [[package]] name = "pyobjc-framework-discrecordingui" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-discrecording" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/6b/3c120c59a939854dd4b7a162fad47011375c5ba00a12940f7217aea90eeb/pyobjc_framework_discrecordingui-11.0.tar.gz", hash = "sha256:bec8a252fd2022dce6c58b1f3366a7295efb0c7c77817f11f9efcce70527d7a2", size = 19614, upload-time = "2025-01-14T19:03:36.695Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/53/d71717f00332b8fc3d8a5c7234fdc270adadfeb5ca9318a55986f5c29c44/pyobjc_framework_discrecordingui-11.1.tar.gz", hash = "sha256:a9f10e2e7ee19582c77f0755ae11a64e3d61c652cbd8a5bf52756f599be24797", size = 19370, upload-time = "2025-06-14T20:57:22.791Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/45/4852afc5e093b76ba8f718d80fe1cc8604122a752806354379a7dbc41dc3/pyobjc_framework_DiscRecordingUI-11.0-py2.py3-none-any.whl", hash = "sha256:1af226c9350bb1d49960c02505e1e2f286e9377040dc2777a3f9a318925e081b", size = 4671, upload-time = "2025-01-14T18:52:16.645Z" }, - { url = "https://files.pythonhosted.org/packages/98/01/c5645513eeaadf0b9e387849fa656fc22524a1881f0d3a44d5b78784f836/pyobjc_framework_DiscRecordingUI-11.0-py3-none-any.whl", hash = "sha256:943df030f497a5ab73e969a04df8a653138fb67ebcf2380fedb4b4886d4ffba0", size = 4736, upload-time = "2025-01-14T18:52:17.655Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a6/505af43f7a17e0ca3d45e099900764e8758e0ca65341e894b74ade513556/pyobjc_framework_discrecordingui-11.1-py2.py3-none-any.whl", hash = "sha256:33233b87d7b85ce277a51d27acca0f5b38485cf1d1dc8e28a065910047766ee2", size = 4721, upload-time = "2025-06-14T20:49:03.737Z" }, ] [[package]] name = "pyobjc-framework-diskarbitration" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/fb/5d3ff093144f499904b1e1bce18d010fe2171b9be62b4679d3dda8b3ad19/pyobjc_framework_diskarbitration-11.0.tar.gz", hash = "sha256:1c3e21398b366a1ce96cf68501a2e415f5ccad4b43a3e7cc901e09e896dfb545", size = 20096, upload-time = "2025-01-14T19:03:37.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/2a/68fa0c99e04ec1ec24b0b7d6f5b7ec735d5e8a73277c5c0671438a69a403/pyobjc_framework_diskarbitration-11.1.tar.gz", hash = "sha256:a933efc6624779a393fafe0313e43378bcae2b85d6d15cff95ac30048c1ef490", size = 19866, upload-time = "2025-06-14T20:57:23.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/f4/f7ad86b2bb922b94745c369b90420cda984e6ad1ac9eb79ec32f5e332123/pyobjc_framework_DiskArbitration-11.0-py2.py3-none-any.whl", hash = "sha256:58823297eb09ff020ee156649170ab824fec32825bd32f2814c32e005920a72c", size = 4793, upload-time = "2025-01-14T18:52:18.561Z" }, - { url = "https://files.pythonhosted.org/packages/8e/87/bf0fc2aa781a819421e572cf6315fae7d0baf46607f9a67c86525c7e0e03/pyobjc_framework_DiskArbitration-11.0-py3-none-any.whl", hash = "sha256:7d41189a2d82045a7195c4661d8ec16195b6325a2f68f9d960e9a9f6649d1131", size = 4865, upload-time = "2025-01-14T18:52:19.786Z" }, + { url = "https://files.pythonhosted.org/packages/1f/72/9534ca88effbf2897e07b722920b3f10890dbc780c6fff1ab4893ec1af10/pyobjc_framework_diskarbitration-11.1-py2.py3-none-any.whl", hash = "sha256:6a8e551e54df481a9081abba6fd680f6633babe5c7735f649731b22896bb6f08", size = 4849, upload-time = "2025-06-14T20:49:04.513Z" }, ] [[package]] name = "pyobjc-framework-dvdplayback" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/89/89ebee4863fd6f173bff9373b5bda4ffa87eba6197337617ab086e23c7d5/pyobjc_framework_dvdplayback-11.0.tar.gz", hash = "sha256:9a005f441afbc34aea301857e166fd650d82762a75d024253e18d1102b21b2f8", size = 64798, upload-time = "2025-01-14T19:03:38.491Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/76/77046325b1957f0cbcdf4f96667496d042ed4758f3413f1d21df5b085939/pyobjc_framework_dvdplayback-11.1.tar.gz", hash = "sha256:b44c36a62c8479e649133216e22941859407cca5796b5f778815ef9340a838f4", size = 64558, upload-time = "2025-06-14T20:57:24.118Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/7f/6073ef2c5170abf55a15750cd069b0c3fdd03e48f3c86761a6a8ecaa0a38/pyobjc_framework_DVDPlayback-11.0-py2.py3-none-any.whl", hash = "sha256:2013289aa38166d81bcbf25d6600ead1996e50de2bc689e5cf36f36a45346424", size = 8171, upload-time = "2025-01-14T18:51:55.282Z" }, - { url = "https://files.pythonhosted.org/packages/db/e4/97ed8d41491f366908581efb8644376fd81ede07ec2cf204cdb3c300ed1e/pyobjc_framework_DVDPlayback-11.0-py3-none-any.whl", hash = "sha256:c6be6ae410d8dce7179d6ee8c9bc421468d4b9c19af3ff0e59c93ae71cfc33e0", size = 8245, upload-time = "2025-01-14T18:51:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/f0fefa171b6938010d87194e26e63eea5c990c33d2d7828de66802f57c36/pyobjc_framework_dvdplayback-11.1-py2.py3-none-any.whl", hash = "sha256:6094e4651ea29540ac817294b27e1596b9d1883d30e78fb5f9619daf94ed30cb", size = 8221, upload-time = "2025-06-14T20:49:05.297Z" }, ] [[package]] name = "pyobjc-framework-eventkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/13/38a98e5cee62e1655d84cfb88cad54fdec4ec272b5fd0c5ac3fc21e33e49/pyobjc_framework_eventkit-11.0.tar.gz", hash = "sha256:3d412203a510b3d62a5eb0987406e0951b13ed39c3351c0ec874afd72496627c", size = 75399, upload-time = "2025-01-14T19:03:39.441Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/c4/cbba8f2dce13b9be37ecfd423ba2b92aa3f209dbb58ede6c4ce3b242feee/pyobjc_framework_eventkit-11.1.tar.gz", hash = "sha256:5643150f584243681099c5e9435efa833a913e93fe9ca81f62007e287349b561", size = 75177, upload-time = "2025-06-14T20:57:24.81Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/d5/e866c951237fb1b6423b85e1623a7f8cc417862261196e276ecc23141976/pyobjc_framework_EventKit-11.0-py2.py3-none-any.whl", hash = "sha256:934e31f4c82f887e1bf01f96d33de4c7c6727de3fdb55bc739e1c686c10cc151", size = 6717, upload-time = "2025-01-14T18:52:20.684Z" }, - { url = "https://files.pythonhosted.org/packages/dc/47/3c0cc7b8c95e6759804b426e78510f65b8e7409c425b85f1b0109d14cdcc/pyobjc_framework_EventKit-11.0-py3-none-any.whl", hash = "sha256:5467977c79649dac9e0183dc72511f7dd49aab0260b67c2cfa25079a5a303f11", size = 6789, upload-time = "2025-01-14T18:52:21.73Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/384b9ff4c6380cac310cb7b92c145896c20a690192dbfc07b38909787ded/pyobjc_framework_eventkit-11.1-py2.py3-none-any.whl", hash = "sha256:c303207610d9c742f4090799f60103cede466002f3c89cf66011c8bf1987750b", size = 6805, upload-time = "2025-06-14T20:49:06.147Z" }, ] [[package]] name = "pyobjc-framework-exceptionhandling" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/46/6c2c4805697a0cfb8413eb7bc6901298e7a1febd49bb1ea960274fc33af3/pyobjc_framework_exceptionhandling-11.0.tar.gz", hash = "sha256:b11562c6eeaef5d8d43e9d817cf50feceb02396e5eb6a7f61df2c0cec93d912b", size = 18157, upload-time = "2025-01-14T19:03:40.393Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/0d/c72a885b40d28a99b586447f9ea6f400589f13d554fcd6f13a2c841bb6d2/pyobjc_framework_exceptionhandling-11.1.tar.gz", hash = "sha256:e010f56bf60ab4e9e3225954ebb53e9d7135d37097043ac6dd2a3f35770d4efa", size = 17890, upload-time = "2025-06-14T20:57:25.521Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/9d/c25b0bc0d300dd5aedd61f0cbd94a91ec6608b550821108d554e9eea0ed7/pyobjc_framework_ExceptionHandling-11.0-py2.py3-none-any.whl", hash = "sha256:972e0a376fee4d3d4c5161f82a8e5f6305392dbf19e98c4c6486d737759ebd89", size = 6993, upload-time = "2025-01-14T18:52:22.621Z" }, - { url = "https://files.pythonhosted.org/packages/cb/04/4b75e083325313e80e66f42d9a932c3febd2db48609d5d960a319b568f7c/pyobjc_framework_ExceptionHandling-11.0-py3-none-any.whl", hash = "sha256:d7f95fdb60a2636416066d3d12fad06cbf597e038576f8ed46fd3c742cc22252", size = 7063, upload-time = "2025-01-14T18:52:24.447Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/dde9c73bf307b62c2d605fc818d3e49f857f39e0841766093dbc9ea47b08/pyobjc_framework_exceptionhandling-11.1-py2.py3-none-any.whl", hash = "sha256:31e6538160dfd7526ac0549bc0fce5d039932aea84c36abbe7b49c79ffc62437", size = 7078, upload-time = "2025-06-14T20:49:07.713Z" }, ] [[package]] name = "pyobjc-framework-executionpolicy" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/91/2e4cacbdabf01bc1207817edacc814b6bc486df12e857a8d86964d98fef4/pyobjc_framework_executionpolicy-11.0.tar.gz", hash = "sha256:de953a8acae98079015b19e75ec8154a311ac1a70fb6d885e17fab09464c98a9", size = 13753, upload-time = "2025-01-14T19:03:42.353Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/cf/54431846508c5d5bb114a415ebb96187da5847105918169e42f4ca3b00e6/pyobjc_framework_executionpolicy-11.1.tar.gz", hash = "sha256:3280ad2f4c5eaf45901f310cee0c52db940c0c63e959ad082efb8df41055d986", size = 13496, upload-time = "2025-06-14T20:57:26.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/03/a433c64c21c754ed796ae5ca0bad63fcb1d51134968ce0c53d4ee806ccd8/pyobjc_framework_ExecutionPolicy-11.0-py2.py3-none-any.whl", hash = "sha256:fdf78bf22fa6ea6f27b574f73856a8a22992d0c0d5a6ed64823e00000c06ffe7", size = 3668, upload-time = "2025-01-14T18:52:28.64Z" }, - { url = "https://files.pythonhosted.org/packages/0b/47/da969dd9d56403e23cc95e68c4816563f64ed6fde7ff4e3c3710e8e8efcf/pyobjc_framework_ExecutionPolicy-11.0-py3-none-any.whl", hash = "sha256:d2dba6f3f7803d1cd0a5608a7ad75085b73097b6c3a935b7f1326c7202249751", size = 3737, upload-time = "2025-01-14T18:52:30.841Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d2/cb192d55786d0f881f2fb60d45b61862a1fcade945f6a7a549ed62f47e61/pyobjc_framework_executionpolicy-11.1-py2.py3-none-any.whl", hash = "sha256:7d4141e572cb916e73bb34bb74f6f976a8aa0a396a0bffd1cf66e5505f7c76c8", size = 3719, upload-time = "2025-06-14T20:49:08.521Z" }, ] [[package]] name = "pyobjc-framework-extensionkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/98/803e3cb000dac227eb0d223802a0aeb052d34a741e572d9584e7d83afca7/pyobjc_framework_extensionkit-11.0.tar.gz", hash = "sha256:82d9e79532e5a0ff0eadf1ccac236c5d3dca344e1090a0f3e88519faa24143c7", size = 19200, upload-time = "2025-01-14T19:03:43.188Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/7d/89adf16c7de4246477714dce8fcffae4242778aecd0c5f0ad9904725f42c/pyobjc_framework_extensionkit-11.1.tar.gz", hash = "sha256:c114a96f13f586dbbab8b6219a92fa4829896a645c8cd15652a6215bc8ff5409", size = 19766, upload-time = "2025-06-14T20:57:27.106Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/1d/ed580ce024d7e9a1ea88ee592d03b34f0b688414793bf8b7be5a367ecea8/pyobjc_framework_ExtensionKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98957dd51f0a4e02aa3d9d3a184f37ca5f99f4cb9e11282a2fc793d18de02af8", size = 7781, upload-time = "2025-01-14T18:52:32.843Z" }, - { url = "https://files.pythonhosted.org/packages/fd/9e/a68989bf7bbba7b5fb1ade168d2179e37164439daaad63a27ccb790a6593/pyobjc_framework_ExtensionKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e341979ee4a7fc5978fe44d6d1d461c774411042cac4e119a32704d6c989de6f", size = 7783, upload-time = "2025-01-14T18:52:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/0f/90/e6607b779756e039c0a4725a37cf70dc5b13c54a8cedbcf01ec1608866b1/pyobjc_framework_extensionkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fd9f9758f95bcff2bf26fe475f679dfff9457d7130f114089e88fd5009675a", size = 7894, upload-time = "2025-06-14T20:49:10.593Z" }, + { url = "https://files.pythonhosted.org/packages/90/2a/93105b5452d2ff680a47e38a3ec6f2a37164babd95e0ab976c07984366de/pyobjc_framework_extensionkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d505a64617c9db4373eb386664d62a82ba9ffc909bffad42cb4da8ca8e244c66", size = 7914, upload-time = "2025-06-14T20:49:11.842Z" }, ] [[package]] name = "pyobjc-framework-externalaccessory" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/b0/ac0a02fe26e66c33fee751a65c1ed06bbd2934db8636e08bb491e8334bad/pyobjc_framework_externalaccessory-11.0.tar.gz", hash = "sha256:39e59331ced75cdcccf23bb5ffe0fa9d67e0c190c1da8887a0e4349b7e27584f", size = 22577, upload-time = "2025-01-14T19:03:44.021Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/a3/519242e6822e1ddc9e64e21f717529079dbc28a353474420da8315d0a8b1/pyobjc_framework_externalaccessory-11.1.tar.gz", hash = "sha256:50887e948b78a1d94646422c243ac2a9e40761675e38b9184487870a31e83371", size = 23123, upload-time = "2025-06-14T20:57:27.845Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/96/bddfe9f72a59a3038ec3208a7d2a62332d5e171d7e3c338ccff6bd6e76b8/pyobjc_framework_ExternalAccessory-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:319f66edb96505f833fe7fe9ba810cb3b0d0c65605b8674bea52f040e8caebd6", size = 8785, upload-time = "2025-01-14T18:52:40.529Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e2/26e9cbb18723200ef71580e46c46f037b7feecc07cf50051cd6fcb426472/pyobjc_framework_ExternalAccessory-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:aaae920c9241d1b35a58ba76dba761689b248250d782179526f6dea151b1fda0", size = 8808, upload-time = "2025-01-14T18:52:42.142Z" }, + { url = "https://files.pythonhosted.org/packages/63/54/d532badd43eba2db3fed2501b8e47a57cab233de2090ee97f4cff723e706/pyobjc_framework_externalaccessory-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2b22f72b83721d841e5a3128df29fc41d785597357c6bbce84555a2b51a1e9d", size = 8887, upload-time = "2025-06-14T20:49:17.703Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1b/e2def12aca9162b0fe0bbf0790d35595d46b2ef12603749c42af9234ffca/pyobjc_framework_externalaccessory-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:00caf75b959db5d14118d78c04085e2148255498839cdee735a0b9f6ef86b6a2", size = 8903, upload-time = "2025-06-14T20:49:18.393Z" }, ] [[package]] name = "pyobjc-framework-fileprovider" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/fc/b8593d8645b9933e60a885f451d0c12d9c0e1b00e62121d8660d95852dff/pyobjc_framework_fileprovider-11.0.tar.gz", hash = "sha256:dcc3ac3c90117c1b8027ea5f26dad6fe5045f688ce3e60d07ece12ec56e17ab3", size = 78701, upload-time = "2025-01-14T19:03:44.931Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/80/3ebba2c1e5e3aeae989fe038c259a93e7e7e18fd56666ece514d000d38ea/pyobjc_framework_fileprovider-11.1.tar.gz", hash = "sha256:748ca1c75f84afdf5419346a24bf8eec44dca071986f31f00071dc191b3e9ca8", size = 91696, upload-time = "2025-06-14T20:57:28.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/57/1f959ec54650d1afc08e89d2995a1534f44229b1371cf66429a45b27c32d/pyobjc_framework_FileProvider-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c7e803a37f7327c191a4de7dbb36e5fbf8bd08dadbcc7f626e491451c7a3849", size = 19179, upload-time = "2025-01-14T18:53:01.659Z" }, - { url = "https://files.pythonhosted.org/packages/30/79/ff4dfe06eb43c97bd723f066ef2b92b00b1020206b4dcc5abe9b49746cad/pyobjc_framework_FileProvider-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d7acdc5e0f4b5488bcbf47d3eea469b22897a4b783fe3f5d4b2b1f3288e82038", size = 19154, upload-time = "2025-01-14T18:53:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e4/c7b985d1199e3697ab5c3247027fe488b9d81b1fb597c34350942dc5838c/pyobjc_framework_fileprovider-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:888d6fb3fd625889ce0e409320c3379330473a386095cb4eda2b4caf0198ff66", size = 19546, upload-time = "2025-06-14T20:49:23.436Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/859d733b0110e56511478ba837fd8a7ba43aa8f8c7e5231b9e3f0258bfbf/pyobjc_framework_fileprovider-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ce6092dfe74c78c0b2abc03bfc18a0f5d8ddc624fc6a1d8dfef26d7796653072", size = 19622, upload-time = "2025-06-14T20:49:24.162Z" }, ] [[package]] name = "pyobjc-framework-fileproviderui" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-fileprovider" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/9d/ca4aed36e6188623e9da633634af772f239bee74934322e1c19ae7b79a53/pyobjc_framework_fileproviderui-11.0.tar.gz", hash = "sha256:cf5c7d32b29d344b65217397eea7b1a2913ce52ce923c9e04135a7a298848d04", size = 13419, upload-time = "2025-01-14T19:03:46.016Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/ed/0f5af06869661822c4a70aacd674da5d1e6b6661240e2883bbc7142aa525/pyobjc_framework_fileproviderui-11.1.tar.gz", hash = "sha256:162a23e67f59e1bb247e84dda88d513d7944d815144901a46be6fe051b6c7970", size = 13163, upload-time = "2025-06-14T20:57:29.568Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/2e/8a91cfa9485a2e9ad295da8bb5505d0dc1046dec8557d2ae17eef75f3912/pyobjc_framework_FileProviderUI-11.0-py2.py3-none-any.whl", hash = "sha256:5102651febb5a6140f99b116b73d0fd6c9822372a5203506e4904ac0ebb1313c", size = 3642, upload-time = "2025-01-14T18:53:06.378Z" }, - { url = "https://files.pythonhosted.org/packages/75/9b/a542159b1aefedb24f01440a929b7bbc6f4bbae3a74d09ad05a7f4adb9c0/pyobjc_framework_FileProviderUI-11.0-py3-none-any.whl", hash = "sha256:b75f70eef2af3696f3cb2e0de88bbb437343b53070078573ae72d64bf56fce9d", size = 3712, upload-time = "2025-01-14T18:53:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/62/01/667e139a0610494e181fccdce519f644166f3d8955b330674deba5876f0d/pyobjc_framework_fileproviderui-11.1-py2.py3-none-any.whl", hash = "sha256:f2765f114c2f4356aa41fb45c621fa8f0a4fae0b6d3c6b1a274366f5fe7fe829", size = 3696, upload-time = "2025-06-14T20:49:29.404Z" }, ] [[package]] name = "pyobjc-framework-findersync" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/e3/24df6e24b589073815be13f2943b93feb12afbf558f6e54c4033b57c29ee/pyobjc_framework_findersync-11.0.tar.gz", hash = "sha256:8dab3feff5debd6bc3746a21ded991716723d98713d1ba37cec1c5e2ad78ee63", size = 15295, upload-time = "2025-01-14T19:03:46.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/82/c6b670494ac0c4cf14cf2db0dfbe0df71925d20595404939383ddbcc56d3/pyobjc_framework_findersync-11.1.tar.gz", hash = "sha256:692364937f418f0e4e4abd395a09a7d4a0cdd55fd4e0184de85ee59642defb6e", size = 15045, upload-time = "2025-06-14T20:57:30.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/f1/42797ae9065e0127df4b5bb7a45e06eff8568a476edbc8d590cea9d25228/pyobjc_framework_FinderSync-11.0-py2.py3-none-any.whl", hash = "sha256:cafb262d1ad1e3a86af333f673aeda4f9bdcf528ded97c2232fd1cf440d1db5a", size = 4788, upload-time = "2025-01-14T18:53:09.559Z" }, - { url = "https://files.pythonhosted.org/packages/d8/96/2ed2ca5536f76102ea3bfb886cdc7b34ec51f53b122b9c535b4ac9b1ee03/pyobjc_framework_FinderSync-11.0-py3-none-any.whl", hash = "sha256:d00285b85038c5546e8566bec9cd3a4615708f0e6cb774d0ea804c69546ec915", size = 4860, upload-time = "2025-01-14T18:53:11.765Z" }, + { url = "https://files.pythonhosted.org/packages/61/10/748ff914c5b7fbae5fa2436cd44b11caeabb8d2f6f6f1b9ab581f70f32af/pyobjc_framework_findersync-11.1-py2.py3-none-any.whl", hash = "sha256:c72b0fd8b746b99cfa498da36c5bb333121b2080ad73fa8cbea05cd47db1fa82", size = 4873, upload-time = "2025-06-14T20:49:30.194Z" }, ] [[package]] name = "pyobjc-framework-fsevents" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/37/4c09cc7b8678e2bb5b68ebc62e817eb88c409b1c41bdc1510d7d24a0372d/pyobjc_framework_fsevents-11.0.tar.gz", hash = "sha256:e01dab04704a518e4c3e1f7d8722819a4f228d5082978e11618aa7abba3883fe", size = 29078, upload-time = "2025-01-14T19:03:49.762Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/83/ec0b9ba355dbc34f27ed748df9df4eb6dbfdd9bbd614b0f193752f36f419/pyobjc_framework_fsevents-11.1.tar.gz", hash = "sha256:d29157d04124503c4dfa9dcbbdc8c34d3bab134d3db3a48d96d93f26bd94c14d", size = 29587, upload-time = "2025-06-14T20:57:30.796Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/8a/75fd630865c9f9d69b1364208582872fc818b4c1a70fd9ae85a5cf7a2c5a/pyobjc_framework_FSEvents-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0b3c7835251a35453e3079cf929b9e5329d02e2f4eaac2ebabbe19e1abd18ab", size = 13209, upload-time = "2025-01-14T18:52:49.478Z" }, - { url = "https://files.pythonhosted.org/packages/19/c6/cae1a6a96ad493339e9f0f175bcf18c1526abe422b63309d873acd663dc2/pyobjc_framework_FSEvents-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb8a5a7f7b5a70e15dae80672f10ecc16b5d1c1afe62ad2ccadb17a8098876cd", size = 13274, upload-time = "2025-01-14T18:52:51.588Z" }, + { url = "https://files.pythonhosted.org/packages/14/6a/25118832a128db99a53be4c45f473192f72923d9b9690785539cee1a9858/pyobjc_framework_fsevents-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:95cc5d839d298b8e95175fb72df8a8e1b08773fd2e0d031efe91eee23e0c8830", size = 13076, upload-time = "2025-06-14T20:49:32.269Z" }, + { url = "https://files.pythonhosted.org/packages/13/c7/378d78e0fd956370f2b120b209117384b5b98925c6d8210a33fd73db4a15/pyobjc_framework_fsevents-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8b51d120b8f12a1ca94e28cf74113bf2bfd4c5aee7035b452e895518f4df7630", size = 13147, upload-time = "2025-06-14T20:49:33.022Z" }, +] + +[[package]] +name = "pyobjc-framework-fskit" +version = "11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/47/d1f04c6115fa78936399a389cc5e0e443f8341c9a6c1c0df7f6fdbe51286/pyobjc_framework_fskit-11.1.tar.gz", hash = "sha256:9ded1eab19b4183cb04381e554bbbe679c1213fd58599d6fc6e135e93b51136f", size = 42091, upload-time = "2025-06-14T20:57:31.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/76/1152bd8121ef2c9a0ccdf10624d647095ce944d34f654f001b458edef668/pyobjc_framework_fskit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:59a939ac8442d648f73a3da75923aa3637ac4693850d995f1914260c8f4f7947", size = 19922, upload-time = "2025-06-14T20:49:38.424Z" }, + { url = "https://files.pythonhosted.org/packages/59/8f/db8f03688db77bfa4b78e89af1d89e910c5e877e94d58bdb3e93cc302e5d/pyobjc_framework_fskit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1e50b8f949f1386fada73b408463c87eb81ef7fd0b3482bacf0c206a73723013", size = 19948, upload-time = "2025-06-14T20:49:39.18Z" }, ] [[package]] name = "pyobjc-framework-gamecenter" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/3b/e66caebc948d9fe3b2671659caab220aff6d5e80ac25442d83331b523d23/pyobjc_framework_gamecenter-11.0.tar.gz", hash = "sha256:18a05500dbcf2cca4a0f05839ec010c76ee08ab65b65020c9538a31feb274483", size = 31459, upload-time = "2025-01-14T19:03:50.766Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/8e/b594fd1dc32a59462fc68ad502be2bd87c70e6359b4e879a99bcc4beaf5b/pyobjc_framework_gamecenter-11.1.tar.gz", hash = "sha256:a1c4ed54e11a6e4efba6f2a21ace92bcf186e3fe5c74a385b31f6b1a515ec20c", size = 31981, upload-time = "2025-06-14T20:57:32.192Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/7e/8a41ab9880e415143baf771d55566e2a863ec538837480a5ee17e1ddc08b/pyobjc_framework_GameCenter-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f8bff2a36cf3cb52cbe321203147766e95997f881062143171cdd8ef2fde9e53", size = 18472, upload-time = "2025-01-14T18:53:13.794Z" }, - { url = "https://files.pythonhosted.org/packages/c4/78/846aa21be2303cba955aaf781a362504a722183b8f6a030ba02f2b2073ad/pyobjc_framework_GameCenter-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de57380e3b51579a6e8bc397c2bb5be5d0f6dcd4bf5abed587700cf7f57afd4", size = 18437, upload-time = "2025-01-14T18:53:14.802Z" }, + { url = "https://files.pythonhosted.org/packages/21/a8/8d9c2d0ff9f42a0951063a9eaff1e39c46c15e89ce4e5e274114340ca976/pyobjc_framework_gamecenter-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:81abe136292ea157acb6c54871915fe6d386146a9386179ded0b974ac435045c", size = 18601, upload-time = "2025-06-14T20:49:44.946Z" }, + { url = "https://files.pythonhosted.org/packages/99/52/0e56f21a6660a4f43882ec641b9e19b7ea92dc7474cec48cda1c9bed9c49/pyobjc_framework_gamecenter-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:779cdf8f52348be7f64d16e3ea37fd621d5ee933c032db3a22a8ccad46d69c59", size = 18634, upload-time = "2025-06-14T20:49:45.737Z" }, ] [[package]] name = "pyobjc-framework-gamecontroller" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/30/02ca5a4fb911acf3e8018abcbd29631a842aeac02958ae91fab1acb13ad1/pyobjc_framework_gamecontroller-11.0.tar.gz", hash = "sha256:6d62f4493d634eba03a43a14c4d1e4511e1e3a2ca2e9cbefa6ae9278a272c1d0", size = 115318, upload-time = "2025-01-14T19:03:52.264Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/4c/1dd62103092a182f2ab8904c8a8e3922d2b0a80a7adab0c20e5fd0207d75/pyobjc_framework_gamecontroller-11.1.tar.gz", hash = "sha256:4d5346faf90e1ebe5602c0c480afbf528a35a7a1ad05f9b49991fdd2a97f105b", size = 115783, upload-time = "2025-06-14T20:57:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/ec/05f356ab2d747a385c2a68908f2f67ee1b1e7a169b1497b0771b2226a174/pyobjc_framework_GameController-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:30e8f251be49ff67491df758c73149e7c7e87bee89919966ed1b2bf56fdaacf7", size = 20995, upload-time = "2025-01-14T18:53:19.979Z" }, - { url = "https://files.pythonhosted.org/packages/66/b3/38319c9232e3508297bfedde700b125676845b1e27afe2bb681e8829f34a/pyobjc_framework_GameController-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:46403f23aaaf6a2e1a51e3954c53d6e910b80058117fdcf3a0a8100f25e30f07", size = 20919, upload-time = "2025-01-14T18:53:20.943Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8e/09e73e03e9f57e77df58cf77f6069d3455a3c388a890ff815e86d036ae39/pyobjc_framework_gamecontroller-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:782779f080508acf869187c0cbd3a48c55ee059d3a14fe89ccd6349537923214", size = 20825, upload-time = "2025-06-14T20:49:51.565Z" }, + { url = "https://files.pythonhosted.org/packages/40/e3/e35bccb0284046ef716db4897b70d061b8b16c91fb2c434b1e782322ef56/pyobjc_framework_gamecontroller-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2cbc0c6c7d9c63e6b5b0b124d0c2bad01bb4b136f3cbc305f27d31f8aab6083", size = 20850, upload-time = "2025-06-14T20:49:52.401Z" }, ] [[package]] name = "pyobjc-framework-gamekit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/df/c161460e5736a34f9b59aa0a3f2d6ad1d1cd9a913aa63c89c41a6ba3b6ae/pyobjc_framework_gamekit-11.0.tar.gz", hash = "sha256:29b5464ca78f0de62e6b6d56e80bbeccb96dc13820b6d5b4e835ab1cc127e5b9", size = 164394, upload-time = "2025-01-14T19:03:53.762Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/7b/ba141ec0f85ca816f493d1f6fe68c72d01092e5562e53c470a0111d9c34b/pyobjc_framework_gamekit-11.1.tar.gz", hash = "sha256:9b8db075da8866c4ef039a165af227bc29393dc11a617a40671bf6b3975ae269", size = 165397, upload-time = "2025-06-14T20:57:33.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/4d/9fe843671c7b94d8e8a925662775d4b2632c138c6a0a9d1bb2c379f225c0/pyobjc_framework_GameKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dabe856c8638940d2b346abc7a1828cca12d00b47d2951d0ac9f4e27ecc8d3ec", size = 21667, upload-time = "2025-01-14T18:53:27.239Z" }, - { url = "https://files.pythonhosted.org/packages/98/b2/d4d1f123fead83bf68eb4ecfab2125933f3114eaf2ed420d7bb99238ba67/pyobjc_framework_GameKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:40d506505f71ed57779c8be9b4e07ec9337c45aebe323b3f8dd8f8c75e6fce50", size = 21627, upload-time = "2025-01-14T18:53:28.228Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2a/f206682b9ff76983bae14a479a9c8a9098e58efc3db31f88211d6ad4fd42/pyobjc_framework_gamekit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5e07c25eab051905c6bd46f368d8b341ef8603dce588ff6dbd82d609dd4fbf71", size = 21932, upload-time = "2025-06-14T20:49:58.154Z" }, + { url = "https://files.pythonhosted.org/packages/1f/23/094e4fe38f2de029365604f0b7dffde7b0edfc57c3d388294c20ed663de2/pyobjc_framework_gamekit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f945c7cfe53c4a349a03a1272f2736cc5cf88fe9e7a7a407abb03899635d860c", size = 21952, upload-time = "2025-06-14T20:49:58.933Z" }, ] [[package]] name = "pyobjc-framework-gameplaykit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-spritekit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/f0/980c4fc3c594d9726b7eb6ae83f73127b22560e1541c7d272d23d17fdf0d/pyobjc_framework_gameplaykit-11.0.tar.gz", hash = "sha256:90eeec464fba992d75a406ccbddb35ed7420a4f5226f19c018982fa3ba7bf431", size = 72837, upload-time = "2025-01-14T19:03:56.127Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/07/f38b1d83eac10ea4f75c605ffc4850585740db89b90842d311e586ee36cd/pyobjc_framework_gameplaykit-11.1.tar.gz", hash = "sha256:9ae2bee69b0cc1afa0e210b4663c7cdbb3cc94be1374808df06f98f992e83639", size = 73399, upload-time = "2025-06-14T20:57:34.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/e7/3530071bf1897f2fe2e5f0c54620f0df9fcac586b9ba6bb5726fc9d295c2/pyobjc_framework_GameplayKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:01f59bbf5beb0cfcfea17011987995f1cccf2ec081d91269f95e71283dd83c67", size = 13381, upload-time = "2025-01-14T18:53:34.217Z" }, - { url = "https://files.pythonhosted.org/packages/b9/07/075369dd9d4e3849646285d4083a9d28214fdd043b499c7929047b942c7f/pyobjc_framework_GameplayKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f2d56af0a84439b3ddc64cdec90e3fab08b1d43da97bed0fb8d60714f47c4372", size = 13382, upload-time = "2025-01-14T18:53:36.303Z" }, + { url = "https://files.pythonhosted.org/packages/0f/29/df66f53f887990878b2b00b1336e451a15e360a384be74559acf47854bc3/pyobjc_framework_gameplaykit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ac9f50941988c30175149af481a49b2026c56a9a497c6dbf2974ffb50ffe0af8", size = 13065, upload-time = "2025-06-14T20:50:05.243Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f5/65bdbefb9de7cbc2edf0b1f76286736536e31c216cfac1a5f84ea15f0fc1/pyobjc_framework_gameplaykit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e4f34db8177b8b4d89fd22a2a882a6c9f6e50cb438ea2fbbf96845481bcd80d", size = 13091, upload-time = "2025-06-14T20:50:05.962Z" }, ] [[package]] name = "pyobjc-framework-healthkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/2f/d79d2ec7c23bfc94bfaa7b7c6f6487a8bffdb73263eea6900aab56135889/pyobjc_framework_healthkit-11.0.tar.gz", hash = "sha256:e78ccb05f747ae3e70b5d73522030b7ba01ef2d390155fba7d50c1c614ae241f", size = 201558, upload-time = "2025-01-14T19:03:57.117Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/66/fa76f7c8e36e4c10677d42d91a8e220c135c610a06b759571db1abe26a32/pyobjc_framework_healthkit-11.1.tar.gz", hash = "sha256:20f59bd9e1ffafe5893b4eff5867fdfd20bd46c3d03bc4009219d82fc6815f76", size = 202009, upload-time = "2025-06-14T20:57:35.285Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/66/36a2fa7ef61b54a8e283355518ed003aa28b26e1dfad9ecbb7f543a08acd/pyobjc_framework_HealthKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ee87453c28bd4040b12a3bc834f0ea1989e331400845a14079e8f4a6ede70aa2", size = 20139, upload-time = "2025-01-14T18:53:44.342Z" }, - { url = "https://files.pythonhosted.org/packages/5f/fd/95d40483d9d185317adbf8433d0c7e83ba36ec6c5a824159b87160f6cebe/pyobjc_framework_HealthKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:680da6d67b0c79d15e897f1c588a8b02d780573aef3692e982294c43727eecf3", size = 20163, upload-time = "2025-01-14T18:53:45.278Z" }, + { url = "https://files.pythonhosted.org/packages/70/aa/c337d27dd98ffcbba2b1200126fcf624d1ccbeb7a4ed9205d48bfe2c1ca8/pyobjc_framework_healthkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:34bce3d144c461af7e577fcf6bbb7739d0537bf42f081960122923a7ef2e06c0", size = 20301, upload-time = "2025-06-14T20:50:12.158Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/12fca070ad2dc0b9c311df209b9b6d275ee192cb5ccbc94616d9ddd80d88/pyobjc_framework_healthkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab4350f9fe65909107dd7992b367a6c8aac7dc31ed3d5b52eeb2310367d0eb0b", size = 20311, upload-time = "2025-06-14T20:50:13.271Z" }, ] [[package]] name = "pyobjc-framework-imagecapturecore" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/fe/db1fc3ffd784a9010070cd87a05d7fd2542c400395589341fab5970a01e1/pyobjc_framework_imagecapturecore-11.0.tar.gz", hash = "sha256:f5d185d8c8b564f8b4a815381bcdb424b10d203ba5bdf0fc887085e007df6f7a", size = 99935, upload-time = "2025-01-14T19:03:58.548Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/3b/f4edbc58a8c7394393f8d00d0e764f655545e743ee4e33917f27b8c68e7b/pyobjc_framework_imagecapturecore-11.1.tar.gz", hash = "sha256:a610ceb6726e385b132a1481a68ce85ccf56f94667b6d6e1c45a2cfab806a624", size = 100398, upload-time = "2025-06-14T20:57:36.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fb/29f20521e0df5da0110f1d6a48e4ed3530a2c0b670bf62d89ceeddd42c18/pyobjc_framework_ImageCaptureCore-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3fd78aa4a69e24caed38ae17a69b973e505324d966df86b47441318800a52db9", size = 16611, upload-time = "2025-01-14T18:54:05.308Z" }, - { url = "https://files.pythonhosted.org/packages/0e/ce/404666e27318435a0513dcf64b85d7cd99195b2e822e03796b03af549c52/pyobjc_framework_ImageCaptureCore-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5cc6c6acbfca05977adc0e339e1225d5cd314af2fa455a70baebb54f9fb2b64", size = 16636, upload-time = "2025-01-14T18:54:06.288Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/465741d33757ef2162a1c9e12d6c8a41b5490949a92431c42a139c132303/pyobjc_framework_imagecapturecore-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ede4c15da909a4d819c732a5554b8282a7b56a1b73d82aef908124147921945a", size = 15999, upload-time = "2025-06-14T20:50:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/54ed61e7cd3213549c8e98ca87a6b21afbb428d2c41948ae48ea019bf973/pyobjc_framework_imagecapturecore-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed296c23d3d8d1d9af96a6486d09fb8d294cc318e4a2152e6f134151c76065f8", size = 16021, upload-time = "2025-06-14T20:50:19.836Z" }, ] [[package]] name = "pyobjc-framework-inputmethodkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/e9/13d007285582e598903264a7d25cc6771a2a52d6c2a96a68fe91db0844fb/pyobjc_framework_inputmethodkit-11.0.tar.gz", hash = "sha256:86cd648bf98c4e777c884b7f69ebcafba84866740430d297645bf388eee6ce52", size = 26684, upload-time = "2025-01-14T19:03:59.525Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/32/6a90bba682a31960ba1fc2d3b263e9be26043c4fb7aed273c13647c8b7d9/pyobjc_framework_inputmethodkit-11.1.tar.gz", hash = "sha256:7037579524041dcee71a649293c2660f9359800455a15e6a2f74a17b46d78496", size = 27203, upload-time = "2025-06-14T20:57:37.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/08/18572def66bf1e0ee6d079b45b34f8b4cbf2ab40b3024c351e4bd83cfa4c/pyobjc_framework_InputMethodKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a58273233e236cb9fa2d8d9295017c6bf26d6f47cc3a5dc9ba81f1c1e64a346", size = 9369, upload-time = "2025-01-14T18:54:12.452Z" }, - { url = "https://files.pythonhosted.org/packages/9d/c9/7793b0d7b363548e62499660899893dff2953ae3a56aa5080e9b199d1291/pyobjc_framework_InputMethodKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7869db2b08586e97181ec2b60b8f5b9d3a683097bae4ce4bb29dc3c5709c3f13", size = 9390, upload-time = "2025-01-14T18:54:13.383Z" }, + { url = "https://files.pythonhosted.org/packages/7f/23/a4226040eec8ed930c81073776064f30d627db03e9db5b24720aad8fd14d/pyobjc_framework_inputmethodkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9b0e47c3bc7f1e628c906436c1735041ed2e9aa7cba3f70084b6311c63c508be", size = 9480, upload-time = "2025-06-14T20:50:25.184Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0d/8a570072096fe339702e4ae9d98e59ee7c6c14124d4437c9a8c4482dda6d/pyobjc_framework_inputmethodkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dd0c591a9d26967018a781fa4638470147ef2a9af3ab4a28612f147573eeefba", size = 9489, upload-time = "2025-06-14T20:50:25.875Z" }, ] [[package]] name = "pyobjc-framework-installerplugins" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/f3/0379655e8ea3566002768d5e7b3ccd72ca845390632a8dabf801348af3a7/pyobjc_framework_installerplugins-11.0.tar.gz", hash = "sha256:88ec84e6999e8b2df874758b09878504a4fbfc8471cf3cd589d57e556f5b916e", size = 27687, upload-time = "2025-01-14T19:04:00.515Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/89/9a881e466476ca21f3ff3e8e87ccfba1aaad9b88f7eea4be6d3f05b07107/pyobjc_framework_installerplugins-11.1.tar.gz", hash = "sha256:363e59c7e05553d881f0facd41884f17b489ff443d7856e33dd0312064c746d9", size = 27451, upload-time = "2025-06-14T20:57:37.915Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/db/0f3334648a53c8ad663fd19d5421863cb0b711e38a2eb742798d50ed33ef/pyobjc_framework_InstallerPlugins-11.0-py2.py3-none-any.whl", hash = "sha256:cb21bfd5597233a2de3d8c0a8d50f23cf92c43e8963edf85787430ac3cadf4a3", size = 4716, upload-time = "2025-01-14T18:54:17.885Z" }, - { url = "https://files.pythonhosted.org/packages/f7/56/fe6f50d74d19b0f85035aba977db7039eedbd2de5ac991278a6a5be475a0/pyobjc_framework_InstallerPlugins-11.0-py3-none-any.whl", hash = "sha256:2221301f466d30d6fd32c7317560c85926a3ee93f1de52d320e3b3cd826a8f93", size = 4784, upload-time = "2025-01-14T18:54:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/3d/01/45c3d159d671c5f488a40f70aa6791b8483a3ed32b461800990bb5ab4bb3/pyobjc_framework_installerplugins-11.1-py2.py3-none-any.whl", hash = "sha256:f92b06c9595f3c800b7aabf1c1a235bfb4b2de3f5406d5f604d8e2ddd0aecb4e", size = 4798, upload-time = "2025-06-14T20:50:30.799Z" }, ] [[package]] name = "pyobjc-framework-instantmessage" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/4d/6810a1f2039ff24d9498858b3ebb46357d4091aa5cec9ff4e41bbcdb25de/pyobjc_framework_instantmessage-11.0.tar.gz", hash = "sha256:ec5c4c70c9b0e61ae82888067246e4f931e700d625b3c42604e54759d4fbf65c", size = 34027, upload-time = "2025-01-14T19:04:01.405Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/b9/5cec4dd0053b5f63c01211a60a286c47464d9f3e0c81bd682e6542dbff00/pyobjc_framework_instantmessage-11.1.tar.gz", hash = "sha256:c222aa61eb009704b333f6e63df01a0e690136e7e495907e5396882779bf9525", size = 33774, upload-time = "2025-06-14T20:57:38.553Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/41/4c0ec3d59f9930e9c52570f7e26d79055881e0009e07466b4988c107ef7c/pyobjc_framework_InstantMessage-11.0-py2.py3-none-any.whl", hash = "sha256:ce364e4e18ec8551512b7d968c0d950ccf7de4bb470f66fe524f3bc8d23df0d1", size = 5334, upload-time = "2025-01-14T18:54:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/19/d9/e3620a5316c986b27361d2f21dd74b48f70c6f7bfe580075e970ca9d7bd6/pyobjc_framework_InstantMessage-11.0-py3-none-any.whl", hash = "sha256:a2817353eaf8f37fe6063c28006b2a0889892e3de801b51b059c153a9d3f35f8", size = 5402, upload-time = "2025-01-14T18:54:21.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/34/acd618e90036822aaf01080d64558ba93e33e15ed91beb7d1d2aab290138/pyobjc_framework_instantmessage-11.1-py2.py3-none-any.whl", hash = "sha256:a70b716e279135eec5666af031f536c0f32dec57cfeae55cc9ff8457f10d4f3d", size = 5419, upload-time = "2025-06-14T20:50:31.993Z" }, ] [[package]] name = "pyobjc-framework-intents" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/88/07e47b0c5c46fe97c23c883ae7a053c2ca6f6fd6afe851d1c2c784644f0f/pyobjc_framework_intents-11.0.tar.gz", hash = "sha256:6405c816dfed8ffa8b3f8b0fae75f61d64787dbae8db1c475bb4450cf8fdf6b5", size = 447921, upload-time = "2025-01-14T19:04:02.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/af/d7f260d06b79acca8028e373c2fe30bf0be014388ba612f538f40597d929/pyobjc_framework_intents-11.1.tar.gz", hash = "sha256:13185f206493f45d6bd2d4903c2136b1c4f8b9aa37628309ace6ff4a906b4695", size = 448459, upload-time = "2025-06-14T20:57:39.589Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/2e/cd8a4aa10a1d3808dd6f71195a28906c573345673dd92b774fbb8c93dd75/pyobjc_framework_Intents-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a9a445a3d4a0622ebf96c65b0ac0be7cec1e72cf7fd9900cd5ace6acf4e84bce", size = 32021, upload-time = "2025-01-14T18:54:23.325Z" }, - { url = "https://files.pythonhosted.org/packages/4a/0e/05c457dab601e3eb5ed7243a04fede32423f08dd03a08e988611359d55b4/pyobjc_framework_Intents-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1e148accce2c7c9243ff90ab3f1a200f93d93506da9c3a2cd034fd5579cb839a", size = 32008, upload-time = "2025-01-14T18:54:24.317Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1d/10fdbf3b8dd6451465ae147143ba3159397a50ff81aed1eb86c153e987b5/pyobjc_framework_intents-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da2f11ee64c75cfbebb1c2be52a20b3618f32b6c47863809ff64c61e8a1dffb9", size = 32227, upload-time = "2025-06-14T20:50:34.303Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/e6fa5737da42fb1265041bd3bd4f2be96f09294018fabf07139dd9dbc7b9/pyobjc_framework_intents-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a663e2de1b7ae7b547de013f89773963f8180023e36f2cebfe8060395dc34c33", size = 32253, upload-time = "2025-06-14T20:50:35.028Z" }, ] [[package]] name = "pyobjc-framework-intentsui" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-intents" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/96/3b3b367f70a4d0a60d2c6251e4a1f4bf470945ae939e0ba20e6d56d10c7a/pyobjc_framework_intentsui-11.0.tar.gz", hash = "sha256:4ce04f926c823fbc1fba7d9c5b33d512b514396719e6bc50ef65b82774e42bc5", size = 20774, upload-time = "2025-01-14T19:04:03.648Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/46/20aae4a71efb514b096f36273a6129b48b01535bf501e5719d4a97fcb3a5/pyobjc_framework_intentsui-11.1.tar.gz", hash = "sha256:c8182155af4dce369c18d6e6ed9c25bbd8110c161ed5f1b4fb77cf5cdb99d135", size = 21305, upload-time = "2025-06-14T20:57:40.477Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/19/f32a14585e749258bb945805da93fd71e05534b14e09fab243fb5ec507ff/pyobjc_framework_IntentsUI-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c677225d38fffc5e00df803f93a6a627c466b35a362ed27173f7901e185882e", size = 8772, upload-time = "2025-01-14T18:54:29.958Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d4/e81e9cfafef63cef481ab251a961ca98e176ca244be91368e0f6b6fe8793/pyobjc_framework_IntentsUI-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b93a1d9594f471596f255db354c13d67caed7aa020afb9f4e69cde2674f4db71", size = 8789, upload-time = "2025-01-14T18:54:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/31/e3/db74fc161bb85bc442dfddf50321924613b67cf49288e2a8b335bf6d546a/pyobjc_framework_intentsui-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:252f7833fabb036cd56d59b445922b25cda1561b54c0989702618a5561d8e748", size = 8936, upload-time = "2025-06-14T20:50:40.522Z" }, + { url = "https://files.pythonhosted.org/packages/43/7c/77fbd2a6f85eb905fbf27ba7540eaf2a026771ed5100fb1c01143cf47e9b/pyobjc_framework_intentsui-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:99a3ae40eb2a6ef1125955dd513c8acc88ce7d8d90130a8cdeaec8336e6fbec5", size = 8965, upload-time = "2025-06-14T20:50:41.281Z" }, ] [[package]] name = "pyobjc-framework-iobluetooth" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/46/62913f8e5ac307b154b3dd50a7a0b167c9d7ac2a579223e33208c141c387/pyobjc_framework_iobluetooth-11.0.tar.gz", hash = "sha256:869f01f573482da92674abbae4a154143e993b1fe4b2c3523f9e0f9c48b798d4", size = 300463, upload-time = "2025-01-14T19:04:04.582Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/e0/74b7b10c567b66c5f38b45ab240336325a4c889f43072d90f2b90aaeb7c0/pyobjc_framework_iobluetooth-11.1.tar.gz", hash = "sha256:094fd4be60cd1371b17cb4b33a3894e0d88a11b36683912be0540a7d51de76f1", size = 300992, upload-time = "2025-06-14T20:57:41.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/2e/2037b1c3459008ccdc41d65ab236d7919eed9bbadd0f02f65dc0193bb170/pyobjc_framework_IOBluetooth-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7d2005d3eff2afed4b5930613ae3c1b50004ebabffb86c0d5dd28d54436e16e6", size = 41010, upload-time = "2025-01-14T18:53:51.62Z" }, - { url = "https://files.pythonhosted.org/packages/2a/52/c266636ff3edc98c1aaf2cc154392876a68d4167bed0351dc2933d5ccc3c/pyobjc_framework_IOBluetooth-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f86d2e675ee2a61ba3d2a446322e918e8ef2dc3e242e893ef81abfc480a6f2c2", size = 41012, upload-time = "2025-01-14T18:53:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/0a/13/31a514e48bd54880aadb1aac3a042fca5f499780628c18f4f54f06d4ece2/pyobjc_framework_iobluetooth-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7d8858cf2e4b2ef5e8bf29b76c06d4f2e6a2264c325146d07dfab94c46633329", size = 40378, upload-time = "2025-06-14T20:50:46.298Z" }, + { url = "https://files.pythonhosted.org/packages/da/94/eef57045762e955795a4e3312674045c52f8c506133acf9efe1b3370b93f/pyobjc_framework_iobluetooth-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:883781e7223cb0c63fab029d640721ded747f2e2b067645bc8b695ef02a4a4dd", size = 40406, upload-time = "2025-06-14T20:50:47.101Z" }, ] [[package]] name = "pyobjc-framework-iobluetoothui" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-iobluetooth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/55/d194de8cfa63c96970e6c90c35e80ce3fceb42934a85d3728736a0e416ff/pyobjc_framework_iobluetoothui-11.0.tar.gz", hash = "sha256:a583758d3e54149ee2dcf00374685aa99e8ae407e044f7c378acc002f9f27e63", size = 23091, upload-time = "2025-01-14T19:04:05.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/32/872272faeab6fe471eac6962c75db72ce65c3556e00b4edebdb41aaab7cb/pyobjc_framework_iobluetoothui-11.1.tar.gz", hash = "sha256:060c721f1cd8af4452493e8153b72b572edcd2a7e3b635d79d844f885afee860", size = 22835, upload-time = "2025-06-14T20:57:42.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/75/9401ae099f32a6be2e5759f8d25c573bcf103833343457ca5981153262ab/pyobjc_framework_IOBluetoothUI-11.0-py2.py3-none-any.whl", hash = "sha256:0f94afeb5ecbde07712ea7658a38d6b0e3558154a6bc29c9a33b633f5952b2c3", size = 3972, upload-time = "2025-01-14T18:53:57.925Z" }, - { url = "https://files.pythonhosted.org/packages/11/a3/75e473de9d25084bfbfa4c0ba24edf038956a604d78219894dc0b412e501/pyobjc_framework_IOBluetoothUI-11.0-py3-none-any.whl", hash = "sha256:5bc366a9904532168ac2c49523e7f090f81b6acbb7b8929ffc7855be0b1d4cf7", size = 4043, upload-time = "2025-01-14T18:54:00.086Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ed/35efed52ed3fa698480624e49ee5f3d859827aad5ff1c7334150c695e188/pyobjc_framework_iobluetoothui-11.1-py2.py3-none-any.whl", hash = "sha256:3c5a382d81f319a1ab9ab11b7ead04e53b758fdfeb604755d39c3039485eaac6", size = 4026, upload-time = "2025-06-14T20:50:52.018Z" }, ] [[package]] name = "pyobjc-framework-iosurface" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/91/ae9ca9e1a777eb786d9d43649437d01d24386736cffe9bb2f504b57e8db6/pyobjc_framework_iosurface-11.0.tar.gz", hash = "sha256:24da8d1cf9356717b1c7e75a1c61e9a9417b62f051d13423a4a7b0978d3dcda5", size = 20555, upload-time = "2025-01-14T19:04:09.475Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/ce/38ec17d860d0ee040bb737aad8ca7c7ff46bef6c9cffa47382d67682bb2d/pyobjc_framework_iosurface-11.1.tar.gz", hash = "sha256:a468b3a31e8cd70a2675a3ddc7176ab13aa521c035f11188b7a3af8fff8b148b", size = 20275, upload-time = "2025-06-14T20:57:42.742Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/08/b96f84b623e2dd2ef733ccdd67a1694f51bfdb4dfd81d38e7755566ab9e5/pyobjc_framework_IOSurface-11.0-py2.py3-none-any.whl", hash = "sha256:58c6e79401a00dc63a5797cd3cc067542d4f94fcd2fc8979dc248c3b06c3b829", size = 4905, upload-time = "2025-01-14T18:54:00.978Z" }, - { url = "https://files.pythonhosted.org/packages/2d/af/4d7ece43c993369a8593c36e0f239b739b78c01e71d74553a630dadd1599/pyobjc_framework_IOSurface-11.0-py3-none-any.whl", hash = "sha256:f2bc13cbfd178396bde6e7558b05a49f69cce376885a07f645a5dd69d2b578fc", size = 4972, upload-time = "2025-01-14T18:54:03.244Z" }, + { url = "https://files.pythonhosted.org/packages/1d/26/fa912d397b577ee318b20110a3c959e898514a1dce19b4f13f238a31a677/pyobjc_framework_iosurface-11.1-py2.py3-none-any.whl", hash = "sha256:0c36ad56f8ec675dd07616418a2bc29126412b54627655abd21de31bcafe2a79", size = 4948, upload-time = "2025-06-14T20:50:52.801Z" }, ] [[package]] name = "pyobjc-framework-ituneslibrary" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/fe/881ab1058d795fe68ccc1e14df0d5e161601dced15d3be84105ecc44bae6/pyobjc_framework_ituneslibrary-11.0.tar.gz", hash = "sha256:2e15dcfbb9d5e95634ddff153de159a28f5879f1a13fdf95504e011773056c6e", size = 47647, upload-time = "2025-01-14T19:04:11.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/43/aebefed774b434965752f9001685af0b19c02353aa7a12d2918af0948181/pyobjc_framework_ituneslibrary-11.1.tar.gz", hash = "sha256:e2212a9340e4328056ade3c2f9d4305c71f3f6af050204a135f9fa9aa3ba9c5e", size = 47388, upload-time = "2025-06-14T20:57:43.383Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/d2/52d1c71ec91ec299e1324658d023954cf62ce4c275155dc66cd298517ae2/pyobjc_framework_iTunesLibrary-11.0-py2.py3-none-any.whl", hash = "sha256:3836fccec315f5186e4b029b486fd18d4b1f24a4c2e73f2d9f3e157ee66d294d", size = 5147, upload-time = "2025-01-14T19:01:49.97Z" }, - { url = "https://files.pythonhosted.org/packages/dc/97/c23c522d506ae01740c04982a1db5861888056dc65d56876a2de0fc490bc/pyobjc_framework_iTunesLibrary-11.0-py3-none-any.whl", hash = "sha256:bfd40fde3f057318329e5fb6e256051eea3f6cd2e2adb9c1f1f51fcb87deb05a", size = 5210, upload-time = "2025-01-14T19:01:51.573Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/a29150f734b45b7408cc06efb9e2156328ae74624e5c4a7fe95118e13e94/pyobjc_framework_ituneslibrary-11.1-py2.py3-none-any.whl", hash = "sha256:4e87d41f82acb6d98cf70ac3c932a568ceb3c2035383cbf177f54e63de6b815f", size = 5191, upload-time = "2025-06-14T20:50:53.637Z" }, ] [[package]] name = "pyobjc-framework-kernelmanagement" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/ea/8ef534fce78817fc577f18de2b34e363873f785894f2bbbfc694823f5088/pyobjc_framework_kernelmanagement-11.0.tar.gz", hash = "sha256:812479d5f85eae27aeeaa22f64c20b926b28b5b9b2bf31c8eab9496d3e038028", size = 12794, upload-time = "2025-01-14T19:04:14.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/b6/708f10ac16425834cb5f8b71efdbe39b42c3b1009ac0c1796a42fc98cd36/pyobjc_framework_kernelmanagement-11.1.tar.gz", hash = "sha256:e934d1638cd89e38d6c6c5d4d9901b4295acee2d39cbfe0bd91aae9832961b44", size = 12543, upload-time = "2025-06-14T20:57:44.046Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/fe/ad7278325d8c760d5366b08d6162193612a3bf33bb0fa98d83d7dcc41918/pyobjc_framework_KernelManagement-11.0-py2.py3-none-any.whl", hash = "sha256:e2ad0efd00c0dce90fc05efac296733282c482d54ec7c5fdcb86b4fb8dff1eb8", size = 3604, upload-time = "2025-01-14T18:54:36.643Z" }, - { url = "https://files.pythonhosted.org/packages/1e/20/8aff6699bf780c88770214f72e92b9db736de078aa1aaaea45312758116e/pyobjc_framework_KernelManagement-11.0-py3-none-any.whl", hash = "sha256:90baacf8bea2883fd62ffb5d7dc6e6ae43fcc6f444458c884da8d92170fcaa5e", size = 3675, upload-time = "2025-01-14T18:54:37.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cf/17ff988ad1a0e55a4be5336c64220aa620ad19bb2f487a1122e9a864b29e/pyobjc_framework_kernelmanagement-11.1-py2.py3-none-any.whl", hash = "sha256:ec74690bd3383a7945c4a038cc4e1553ec5c1d2408b60e2b0003a3564bff7c47", size = 3656, upload-time = "2025-06-14T20:50:54.484Z" }, ] [[package]] name = "pyobjc-framework-latentsemanticmapping" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/29/8838eefeb82da95931134b06624364812dedf7e9cc905f36d95d497f2904/pyobjc_framework_latentsemanticmapping-11.0.tar.gz", hash = "sha256:6f578c3e0a171706bdbfcfc2c572a8059bf8039d22c1475df13583749a35cec1", size = 17704, upload-time = "2025-01-14T19:04:14.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/8a/4e54ee2bc77d59d770b287daf73b629e2715a2b3b31264d164398131cbad/pyobjc_framework_latentsemanticmapping-11.1.tar.gz", hash = "sha256:c6c3142301e4d375c24a47dfaeebc2f3d0fc33128a1c0a755794865b9a371145", size = 17444, upload-time = "2025-06-14T20:57:44.643Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/87/a8d2f508c021afa4f8af51773ab22cbd883270bfda8368a86d473736b05a/pyobjc_framework_LatentSemanticMapping-11.0-py2.py3-none-any.whl", hash = "sha256:87fd91320fb7ce0b2c482fda41a5c38388f5a694ee2d7208725d22ff75438c00", size = 5369, upload-time = "2025-01-14T18:54:38.493Z" }, - { url = "https://files.pythonhosted.org/packages/df/f0/cea2a0d25ad20aef6eb38c432d2c93bda2cb2239c6286b6086f8687a8072/pyobjc_framework_LatentSemanticMapping-11.0-py3-none-any.whl", hash = "sha256:073b8a4e7a22e6abd58005b7d7091144aec4fc1d4b519e9f972b3aee9da30009", size = 5435, upload-time = "2025-01-14T18:54:39.643Z" }, + { url = "https://files.pythonhosted.org/packages/2c/50/d62815b02968236eb46c33f0fb0f7293a32ef68d2ec50c397140846d4e42/pyobjc_framework_latentsemanticmapping-11.1-py2.py3-none-any.whl", hash = "sha256:57f3b183021759a100d2847a4d8aa314f4033be3d2845038b62e5e823d96e871", size = 5454, upload-time = "2025-06-14T20:50:55.658Z" }, ] [[package]] name = "pyobjc-framework-launchservices" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-coreservices" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/59/eb847389224c670c885ae3d008b1ffe3b996bbe094b43e49dfa84f3947a9/pyobjc_framework_launchservices-11.0.tar.gz", hash = "sha256:7c5c8a8cec013e2cb3fa82a167ca2d61505c36a79f75c718f3f913e597f9ffee", size = 20691, upload-time = "2025-01-14T19:04:15.884Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/0a/a76b13109b8ab563fdb2d7182ca79515f132f82ac6e1c52351a6b02896a8/pyobjc_framework_launchservices-11.1.tar.gz", hash = "sha256:80b55368b1e208d6c2c58395cc7bc12a630a2a402e00e4930493e9bace22b7bb", size = 20446, upload-time = "2025-06-14T20:57:45.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/46/72937390e3eb0f31809f0d56004a388d20b49724495885e8be677707c07c/pyobjc_framework_LaunchServices-11.0-py2.py3-none-any.whl", hash = "sha256:654572e5f2997d8f802b97f619fc6c7d4f927abb03ce53b3dad89b376517b2d1", size = 3807, upload-time = "2025-01-14T18:54:40.579Z" }, - { url = "https://files.pythonhosted.org/packages/c0/12/74b96f187beb2f5605f9d487c3141ac8d25193556f2f5febff3580e8b2cb/pyobjc_framework_LaunchServices-11.0-py3-none-any.whl", hash = "sha256:dbc169442deae53f881d1d07fc79c9da6459e5f0b411e8dd1cfd1c519b3a99c8", size = 3876, upload-time = "2025-01-14T18:54:41.577Z" }, + { url = "https://files.pythonhosted.org/packages/12/30/a4de9021fdef7db0b224cdc1eae75811d889dc1debdfafdabf8be7bd0fb9/pyobjc_framework_launchservices-11.1-py2.py3-none-any.whl", hash = "sha256:8b58f1156651058b2905c87ce48468f4799db86a7edf760e1897fedd057a3908", size = 3889, upload-time = "2025-06-14T20:50:56.484Z" }, ] [[package]] name = "pyobjc-framework-libdispatch" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/33/4ec96a9edd37948f09e94635852c2db695141430cc1adc7b25968e1f3a95/pyobjc_framework_libdispatch-11.0.tar.gz", hash = "sha256:d22df11b07b1c3c8e7cfc4ba9e876a95c19f44acd36cf13d40c5cccc1ffda04b", size = 53496, upload-time = "2025-01-14T19:04:16.82Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/89/7830c293ba71feb086cb1551455757f26a7e2abd12f360d375aae32a4d7d/pyobjc_framework_libdispatch-11.1.tar.gz", hash = "sha256:11a704e50a0b7dbfb01552b7d686473ffa63b5254100fdb271a1fe368dd08e87", size = 53942, upload-time = "2025-06-14T20:57:45.903Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/1f/f3273cc8261d45a6bef1fa48ac39cd94f6a1e77b1ec70f79bae52ad54015/pyobjc_framework_libdispatch-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cebdc33a1a771c9ab03fe5c8a2b0ed9698804e7bccdbfcd3cc0045c4b4aad4f3", size = 20607, upload-time = "2025-01-14T19:01:53.577Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/40638a5e916b1b94b4b29abacb18628fd47871d80fdf2fc1ef7216726d29/pyobjc_framework_libdispatch-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:999815af50ad2216e28d76893023b7839b7f1e8f22bcf7062d81d9a51ade4613", size = 15949, upload-time = "2025-01-14T19:01:54.496Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cd/1010dee9f932a9686c27ce2e45e91d5b6875f5f18d2daafadea70090e111/pyobjc_framework_libdispatch-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ddca472c2cbc6bb192e05b8b501d528ce49333abe7ef0eef28df3133a8e18b7", size = 20441, upload-time = "2025-06-14T20:50:58.3Z" }, + { url = "https://files.pythonhosted.org/packages/ac/92/ff9ceb14e1604193dcdb50643f2578e1010c68556711cd1a00eb25489c2b/pyobjc_framework_libdispatch-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc9a7b8c2e8a63789b7cf69563bb7247bde15353208ef1353fff0af61b281684", size = 15627, upload-time = "2025-06-14T20:50:59.055Z" }, ] [[package]] name = "pyobjc-framework-libxpc" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/7e/9fa73ce6925db9cfd8a6b45d97943af8fe59f92251e7fd201b6e4608c172/pyobjc_framework_libxpc-11.0.tar.gz", hash = "sha256:e0c336913ab6a526b036915aa9038de2a5281e696ac2d3db3347b3040519c11d", size = 48627, upload-time = "2025-01-14T19:04:17.728Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/c9/7e15e38ac23f5bfb4e82bdf3b7ef88e2f56a8b4ad884009bc2d5267d2e1f/pyobjc_framework_libxpc-11.1.tar.gz", hash = "sha256:8fd7468aa520ff19915f6d793070b84be1498cb87224bee2bad1f01d8375273a", size = 49135, upload-time = "2025-06-14T20:57:46.59Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/c2/b77019e344b3f46ca4169c19e0539cff9586c8db0a97715590696993bd00/pyobjc_framework_libxpc-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5f05f9eb3662df5832ff09ab788d6f6099f4674cb015200db317ea8c69f8c5e8", size = 19683, upload-time = "2025-01-14T19:02:02.364Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b9/bf34709c2d8f62a029f4c8e7f9a58c6eb5f3a68542cbcd2a15070b66485a/pyobjc_framework_libxpc-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e000cad8588a961a3e6e5016736cd76b5d992b080cfe8b95745691db5a0ce8df", size = 19788, upload-time = "2025-01-14T19:02:03.327Z" }, + { url = "https://files.pythonhosted.org/packages/39/01/f5fbc7627f838aea5960f3287b75cbda9233f76fc3ba82f088630d5d16cc/pyobjc_framework_libxpc-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ec8a7df24d85a561fc21d0eb0db89e8cddefeedec71c69bccf17f99804068ed", size = 19466, upload-time = "2025-06-14T20:51:05.138Z" }, + { url = "https://files.pythonhosted.org/packages/be/8f/dfd8e1e1e461f857a1e50138e69b17c0e62a8dcaf7dea791cc158d2bf854/pyobjc_framework_libxpc-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c29b2df8d74ff6f489afa7c39f7c848c5f3d0531a6bbe704571782ee6c820084", size = 19573, upload-time = "2025-06-14T20:51:05.902Z" }, ] [[package]] name = "pyobjc-framework-linkpresentation" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/5c/dac9fe4ad0a4076c863b5ac9925e751fc18c637ae411e4891c4b7558a5b3/pyobjc_framework_linkpresentation-11.0.tar.gz", hash = "sha256:bc4ace4aab4da4a4e4df10517bd478b6d51ebf00b423268ee8d9f356f9e87be9", size = 15231, upload-time = "2025-01-14T19:04:20.763Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/76/22873be73f12a3a11ae57af13167a1d2379e4e7eef584de137156a00f5ef/pyobjc_framework_linkpresentation-11.1.tar.gz", hash = "sha256:a785f393b01fdaada6d7d6d8de46b7173babba205b13b44f1dc884b3695c2fc9", size = 14987, upload-time = "2025-06-14T20:57:47.277Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/fc/aa3f0016e2246c4574cce0e323416303992411a012266b5bdda74095ebef/pyobjc_framework_LinkPresentation-11.0-py2.py3-none-any.whl", hash = "sha256:c10ee1ac48bb7cd2d67ade7f354ec71af1f4244a8deb8530ba646fd4ba327b21", size = 3799, upload-time = "2025-01-14T18:54:43.137Z" }, - { url = "https://files.pythonhosted.org/packages/85/0b/77c16f2d4541a4490723e18c03c3bd6ecf7db789cf4988e628753e2e4526/pyobjc_framework_LinkPresentation-11.0-py3-none-any.whl", hash = "sha256:5b063900715c5bcf58f533e6c9672473cb07fe3eaa0f0454d93947defa09f13e", size = 3865, upload-time = "2025-01-14T18:54:44.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/59/23249e76e06e3c1a4f88acac7144999fae5a5a8ce4b90272d08cc0ac38ae/pyobjc_framework_linkpresentation-11.1-py2.py3-none-any.whl", hash = "sha256:018093469d780a45d98f4e159f1ea90771caec456b1599abcc6f3bf3c6873094", size = 3847, upload-time = "2025-06-14T20:51:10.817Z" }, ] [[package]] name = "pyobjc-framework-localauthentication" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-security" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/b1/bea4b5f8adbb69c0b34eddee63e052f35271cc630db43fbef6873352e21f/pyobjc_framework_localauthentication-11.0.tar.gz", hash = "sha256:eb55a3de647894092d6ed3f8f13fdc38e5dbf4850be320ea14dd2ac83176b298", size = 40020, upload-time = "2025-01-14T19:04:22.206Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/27/9e3195f3561574140e9b9071a36f7e0ebd18f50ade9261d23b5b9df8fccd/pyobjc_framework_localauthentication-11.1.tar.gz", hash = "sha256:3cd48907c794bd414ac68b8ac595d83c7e1453b63fc2cfc2d2035b690d31eaa1", size = 40700, upload-time = "2025-06-14T20:57:47.931Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/dd/eaa44e4fe3b5c312190c0468afcab0a4372da29535fe9f860b6b9e1e6b4a/pyobjc_framework_LocalAuthentication-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6500bb5b195799d70f2a622d89a9c2531cb13d6afe30916cf073a195dd86eb", size = 10515, upload-time = "2025-01-14T18:54:46.214Z" }, - { url = "https://files.pythonhosted.org/packages/31/86/f4e913e966a6dbefbaa95aed35e7d235ba2f172d079d3c0b4351a584357b/pyobjc_framework_LocalAuthentication-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c0291e743fb1534c1df900e9adacc809af0651744627ce8ae25cfd021e3db73b", size = 10530, upload-time = "2025-01-14T18:54:47.16Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9a/acc10d45041445db99a121950b0d4f4ff977dbe5e95ec154fe2e1740ff08/pyobjc_framework_localauthentication-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b6d52d07abd2240f7bc02b01ea1c630c280ed3fbc3fabe1e43b7444cfd41788", size = 10707, upload-time = "2025-06-14T20:51:12.436Z" }, + { url = "https://files.pythonhosted.org/packages/91/db/59f118cc2658814c6b501b7360ca4fe6a82fd289ced5897b99787130ceef/pyobjc_framework_localauthentication-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:aa3815f936612d78e51b53beed9115c57ae2fd49500bb52c4030a35856e6569e", size = 10730, upload-time = "2025-06-14T20:51:13.487Z" }, ] [[package]] name = "pyobjc-framework-localauthenticationembeddedui" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-localauthentication" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/ee/821f2d2e9da4cba3dc47e50c8367c6405e91551fb7d8ec842858d5b1d45d/pyobjc_framework_localauthenticationembeddedui-11.0.tar.gz", hash = "sha256:7e9bf6df77ff12a4e827988d8578c15b4431694b2fcfd5b0dad5d7738757ee6a", size = 14204, upload-time = "2025-01-14T19:04:23.566Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/7b/08c1e52487b07e9aee4c24a78f7c82a46695fa883113e3eece40f8e32d40/pyobjc_framework_localauthenticationembeddedui-11.1.tar.gz", hash = "sha256:22baf3aae606e5204e194f02bb205f244e27841ea7b4a4431303955475b4fa56", size = 14076, upload-time = "2025-06-14T20:57:48.557Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/66/2151e5ee7fb97b34c7eda9f8b1442683cced27bcb273d34c8aa2c564e528/pyobjc_framework_LocalAuthenticationEmbeddedUI-11.0-py2.py3-none-any.whl", hash = "sha256:0ccbbdd8c7142b1670885881c803f684ee356df83a5338be9135f46462caae6c", size = 3914, upload-time = "2025-01-14T18:54:52.074Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a9/c362ac3586bb2d46868b8ea9da3747c9aae3f0c9448ee09934a1be805383/pyobjc_framework_LocalAuthenticationEmbeddedUI-11.0-py3-none-any.whl", hash = "sha256:e8da98dc38a88995e344742585d3735af9b5bd9926a29774d77e2aa6dd46b7af", size = 3984, upload-time = "2025-01-14T18:54:54.974Z" }, + { url = "https://files.pythonhosted.org/packages/51/3d/2aaa3a4f0e82f0ac95cc432a6079f6dc20aa18a66c9a87ac6128c70df9ef/pyobjc_framework_localauthenticationembeddedui-11.1-py2.py3-none-any.whl", hash = "sha256:3539a947b102b41ea6e40e7c145f27280d2f36a2a9a1211de32fa675d91585eb", size = 3973, upload-time = "2025-06-14T20:51:18.2Z" }, ] [[package]] name = "pyobjc-framework-mailkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/79/9c9140f726ba14898762ddc19e7142724e0ce5930f08eb20f33f78b05be8/pyobjc_framework_mailkit-11.0.tar.gz", hash = "sha256:d08a2dcc95b5e7955c7c385fe6e018325113d02c007c4178d3fb3c9ab326c163", size = 32274, upload-time = "2025-01-14T19:04:25.086Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/7e/f22d733897e7618bd70a658b0353f5f897c583df04e7c5a2d68b99d43fbb/pyobjc_framework_mailkit-11.1.tar.gz", hash = "sha256:bf97dc44cb09b9eb9d591660dc0a41f077699976144b954caa4b9f0479211fd7", size = 32012, upload-time = "2025-06-14T20:57:49.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/38/f9bcd204c1ba0943365f3cc505d934ea93fe4b99d61e961ced0f0991a4f9/pyobjc_framework_MailKit-11.0-py2.py3-none-any.whl", hash = "sha256:78e54ff3988fd1af16c06e0c39dea3b7ff522e367d262f58e88962772291c7f9", size = 4803, upload-time = "2025-01-14T18:54:58.295Z" }, - { url = "https://files.pythonhosted.org/packages/64/4a/f3596583795c608838c7fa84fc4836f365c5744a3e412392d47a200a6221/pyobjc_framework_MailKit-11.0-py3-none-any.whl", hash = "sha256:0573ee0be66419130774aca36b611d0d07fcf7c756524860acba8fe17eefeec2", size = 4874, upload-time = "2025-01-14T18:55:00.648Z" }, + { url = "https://files.pythonhosted.org/packages/bf/23/1897fc071e8e71bc0bef53bcb0d600eb1ed3bd6c4609f7257ddfe151d37a/pyobjc_framework_mailkit-11.1-py2.py3-none-any.whl", hash = "sha256:8e6026462567baba194468e710e83787f29d9e8c98ea0583f7b401ea9515966e", size = 4854, upload-time = "2025-06-14T20:51:18.978Z" }, ] [[package]] name = "pyobjc-framework-mapkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, @@ -3310,29 +3264,28 @@ dependencies = [ { name = "pyobjc-framework-corelocation" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/7e/ef86c6e218a58bb9497ce9754a77f12ffe01c4b3609279727b7d7e44655a/pyobjc_framework_mapkit-11.0.tar.gz", hash = "sha256:cd8a91df4c0b442fcf1b14d735e566a06b21b3f48a2a4afe269fca45bfa49117", size = 165080, upload-time = "2025-01-14T19:04:26.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/f0/505e074f49c783f2e65ca82174fd2d4348568f3f7281c1b81af816cf83bb/pyobjc_framework_mapkit-11.1.tar.gz", hash = "sha256:f3a5016f266091be313a118a42c0ea4f951c399b5259d93639eb643dacc626f1", size = 165614, upload-time = "2025-06-14T20:57:50.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/7e/f0457c7ca001a01f47aa944c1f86a24d2d04db0aa1c19f51cbf77a65cc9b/pyobjc_framework_MapKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:83128d79aa7644e5b966b32346f7da749b1dbb110dadba857b93ecf5663e24e6", size = 23045, upload-time = "2025-01-14T18:55:02.629Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b0/532b4f57f8783cf6394b17e76174c393d0503ee41e026782a9950bd46279/pyobjc_framework_MapKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e6aa1d00cfe2e02b301467e24ca51e469e9a8a2ec2a9f097b73adca1a5a2a054", size = 23040, upload-time = "2025-01-14T18:55:04.055Z" }, + { url = "https://files.pythonhosted.org/packages/a0/dc/a7e03a9066e6eed9d1707ae45453a5332057950e16de6665402c804ae7af/pyobjc_framework_mapkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:daee6bedc3acc23e62d1e7c3ab97e10425ca57e0c3cc47d2b212254705cc5c44", size = 22481, upload-time = "2025-06-14T20:51:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/30/0a/50aa2fba57499ff657cacb9ef1730006442e4f42d9a822dae46239603ecc/pyobjc_framework_mapkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:91976c6dbc8cbb020e059a0ccdeab8933184712f77164dbad5a5526c1a49599d", size = 22515, upload-time = "2025-06-14T20:51:21.439Z" }, ] [[package]] name = "pyobjc-framework-mediaaccessibility" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/8e/9fe2cb251ff6107a03bafa07f63b6593df145a2579fffb096023fb21b167/pyobjc_framework_mediaaccessibility-11.0.tar.gz", hash = "sha256:1298cc0128e1c0724e8f8e63a6167ea6809a985922c67399b997f8243de59ab4", size = 18671, upload-time = "2025-01-14T19:04:27.624Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/81/60412b423c121de0fa0aa3ef679825e1e2fe8b00fceddec7d72333ef564b/pyobjc_framework_mediaaccessibility-11.1.tar.gz", hash = "sha256:52479a998fec3d079d2d4590a945fc78c41fe7ac8c76f1964c9d8156880565a4", size = 18440, upload-time = "2025-06-14T20:57:51.126Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/1f/36b1115cfd02d68d39cc3fe976fe3d40bad1d1a0a9c8175c66d230bb7276/pyobjc_framework_MediaAccessibility-11.0-py2.py3-none-any.whl", hash = "sha256:901961f171f7af184decbf5a3899debfa56dbd1a63a53d0ff3d93eff90f2f464", size = 4637, upload-time = "2025-01-14T18:55:08.968Z" }, - { url = "https://files.pythonhosted.org/packages/72/3f/fa350681a6599ed6756dc598fcd17fda1521249e4570a57b4a9b9c900f47/pyobjc_framework_MediaAccessibility-11.0-py3-none-any.whl", hash = "sha256:3f4b9e4d1ac8e7f8cdb7a2e9839ab75cb358dead3e6365ccd8d6017d7e93811e", size = 4708, upload-time = "2025-01-14T18:55:09.939Z" }, + { url = "https://files.pythonhosted.org/packages/99/a1/f4cbdf8478ad01859e2c8eef08e28b8a53b9aa4fe5d238a86bad29b73555/pyobjc_framework_mediaaccessibility-11.1-py2.py3-none-any.whl", hash = "sha256:cd07e7fc375ff1e8d225e0aa2bd9c2c1497a4d3aa5a80bfb13b08800fcd7f034", size = 4691, upload-time = "2025-06-14T20:51:26.596Z" }, ] [[package]] name = "pyobjc-framework-mediaextension" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, @@ -3340,284 +3293,276 @@ dependencies = [ { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-coremedia" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/1f/e31d9431bc71077b09583ea863b3c91b7de9371d0cc17a8be99be8119daa/pyobjc_framework_mediaextension-11.0.tar.gz", hash = "sha256:ecd8a64939e1c16be005690117c21fd406fc04d3036e2adea7600d2a0c53f4ea", size = 57931, upload-time = "2025-01-14T19:04:28.65Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/09/fd214dc0cf3f3bc3f528815af4799c0cb7b4bf4032703b19ea63486a132b/pyobjc_framework_mediaextension-11.1.tar.gz", hash = "sha256:85a1c8a94e9175fb364c453066ef99b95752343fd113f08a3805cad56e2fa709", size = 58489, upload-time = "2025-06-14T20:57:51.796Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/94/1e4aa67e424a043dfa886c946bb872f9653cc12ad59bd7c2c24e3d19a4f5/pyobjc_framework_MediaExtension-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f25d674f381bae800761efe1628959293009d287f7127616f75318a87e4543d", size = 39781, upload-time = "2025-01-14T18:55:13.454Z" }, - { url = "https://files.pythonhosted.org/packages/02/3c/2cbd4498950daadd111639a7b8dea2aaa6825526677b31ae49bc940f1036/pyobjc_framework_MediaExtension-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9a167725f7a6921d446084b132505392bb375a5ef91498f7be5d94c0d48d26ae", size = 39777, upload-time = "2025-01-14T18:55:14.434Z" }, + { url = "https://files.pythonhosted.org/packages/ec/25/95315f730e9b73ef9e8936ed3ded636d3ac71b4d5653d4caf1d20a2314a8/pyobjc_framework_mediaextension-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:915c0cbb04913beb1f1ac8939dc0e615da8ddfba3927863a476af49f193415c5", size = 38858, upload-time = "2025-06-14T20:51:28.296Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/2c2d8265851f6060dbf4434c21bd67bf569b8c3071ba1f257e43aae563a8/pyobjc_framework_mediaextension-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:06cb19004413a4b08dd75cf1e5dadea7f2df8d15feeeb7adb529d0cf947fa789", size = 38859, upload-time = "2025-06-14T20:51:29.102Z" }, ] [[package]] name = "pyobjc-framework-medialibrary" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/a4/8c7d1635994800dc412a5db2c4b43ed499184651efcec0c8da3cf8e2bcc7/pyobjc_framework_medialibrary-11.0.tar.gz", hash = "sha256:692889fab1e479a9c207f0ff23c900dad5f47caf47c05cc995d9bb7c1e56e8b9", size = 18975, upload-time = "2025-01-14T19:04:29.739Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/06/11ff622fb5fbdd557998a45cedd2b0a1c7ea5cc6c5cb015dd6e42ebd1c41/pyobjc_framework_medialibrary-11.1.tar.gz", hash = "sha256:102f4326f789734b7b2dfe689abd3840ca75a76fb8058bd3e4f85398ae2ce29d", size = 18706, upload-time = "2025-06-14T20:57:52.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/b6/c079b41a7a4b6b856b4ba7196500f058fb9d9f4f021269b49cf0861ace1f/pyobjc_framework_MediaLibrary-11.0-py2.py3-none-any.whl", hash = "sha256:3d273d4db7e1894fd2a95448c26eeced6e13e33555f727988aeec4b2762246fb", size = 4288, upload-time = "2025-01-14T18:55:20.473Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ae/05f2ee15f5e8524b27d6e446822edfed977c1ed0d3201644ae4d5d78bdde/pyobjc_framework_MediaLibrary-11.0-py3-none-any.whl", hash = "sha256:b8b97bb9067cf81942ce69d3273e2b18d093290c3fd692172a54f012ab64c0b3", size = 4359, upload-time = "2025-01-14T18:55:21.491Z" }, + { url = "https://files.pythonhosted.org/packages/62/2b/a4200080d97f88fdd406119bb8f00ccb7f32794f84735485510c14e87e76/pyobjc_framework_medialibrary-11.1-py2.py3-none-any.whl", hash = "sha256:779be84bd280f63837ce02028ca46b41b090902aa4205887ffd5777f49377669", size = 4340, upload-time = "2025-06-14T20:51:34.339Z" }, ] [[package]] name = "pyobjc-framework-mediaplayer" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-avfoundation" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/ce/3d2783f2f96ddf51bebcf6537a4a0f2a8a1fe4e520de218fc1b7c5b219ed/pyobjc_framework_mediaplayer-11.0.tar.gz", hash = "sha256:c61be0ba6c648db6b1d013a52f9afb8901a8d7fbabd983df2175c1b1fbff81e5", size = 94020, upload-time = "2025-01-14T19:04:30.617Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/d5/daba26eb8c70af1f3823acfd7925356acc4dd75eeac4fc86dc95d94d0e15/pyobjc_framework_mediaplayer-11.1.tar.gz", hash = "sha256:d07a634b98e1b9eedd82d76f35e616525da096bd341051ea74f0971e0f2f2ddd", size = 93749, upload-time = "2025-06-14T20:57:53.165Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/b2/57b7b75bb5f2b624ce48cd48fb7d651d2f24d279918b352ae8fb03384b47/pyobjc_framework_MediaPlayer-11.0-py2.py3-none-any.whl", hash = "sha256:b124b0f18444b69b64142bad2579287d0b1a4a35cb6b14526523a822066d527d", size = 6903, upload-time = "2025-01-14T18:55:24.375Z" }, - { url = "https://files.pythonhosted.org/packages/e9/8e/4969374f0fb243dd06336f2edc8c755743a683e73a57c3253279d048a455/pyobjc_framework_MediaPlayer-11.0-py3-none-any.whl", hash = "sha256:1a051624b536666feb5fd1a4bb54000ab45dac0c8aea4cd4707cbde1773acf57", size = 6977, upload-time = "2025-01-14T18:55:25.359Z" }, + { url = "https://files.pythonhosted.org/packages/2b/aa/b37aac80d821bd2fa347ddad1f6c7c75b23155e500edf1cb3b3740c27036/pyobjc_framework_mediaplayer-11.1-py2.py3-none-any.whl", hash = "sha256:b655cf537ea52d73209eb12935a047301c30239b318a366600f0f44335d51c9a", size = 6960, upload-time = "2025-06-14T20:51:35.171Z" }, ] [[package]] name = "pyobjc-framework-mediatoolbox" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/46/cf5f3bde6cad32f10095850ca44f24ba241d18b26379187c412be1260f39/pyobjc_framework_mediatoolbox-11.0.tar.gz", hash = "sha256:de949a44f10b5a15e5a7131ee53b2806b8cb753fd01a955970ec0f475952ba24", size = 23067, upload-time = "2025-01-14T19:04:32.823Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/68/cc230d2dfdeb974fdcfa828de655a43ce2bf4962023fd55bbb7ab0970100/pyobjc_framework_mediatoolbox-11.1.tar.gz", hash = "sha256:97834addc5179b3165c0d8cd74cc97ad43ed4c89547724216426348aca3b822a", size = 23568, upload-time = "2025-06-14T20:57:53.913Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/d5/ee184e33bd743c363d7ab59d8412289c6ac14c78a035545a067b98704ae2/pyobjc_framework_MediaToolbox-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:df09e4db52d4efeafe4a324600b9c5062fd87c1d1217ebec2df65c8b6b0ce9ef", size = 12776, upload-time = "2025-01-14T18:55:30.277Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a5/c02d2c44ebcd5884d7ccf55c597c0960d14e4e8f386b65dcd76f9f50ec3d/pyobjc_framework_MediaToolbox-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e80e3057f5030fb034ac93c3e891cee346716e1669f280ebbd63ccfa52b2b7ff", size = 12937, upload-time = "2025-01-14T18:55:34.139Z" }, + { url = "https://files.pythonhosted.org/packages/99/bc/6b69ca3c2bf1573b907be460c6a413ff2dfd1c037da53f46aec3bcdb3c73/pyobjc_framework_mediatoolbox-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da60c0409b18dfb9fa60a60589881e1382c007700b99722926270feadcf3bfc1", size = 12630, upload-time = "2025-06-14T20:51:36.873Z" }, + { url = "https://files.pythonhosted.org/packages/b5/23/6b5d999e1e71c42d5d116d992515955ac1bbc5cf4890072bb26f38eb9802/pyobjc_framework_mediatoolbox-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2867c91645a335ee29b47e9c0e9fd3ea8c9daad0c0719c50b8bf244d76998056", size = 12785, upload-time = "2025-06-14T20:51:37.593Z" }, ] [[package]] name = "pyobjc-framework-metal" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/e0/a6d18a1183410a5d8610ca1ae6c065b8944586441f8669faee7509817246/pyobjc_framework_metal-11.0.tar.gz", hash = "sha256:cad390150aa63502d5cfe242026b55ed39ffaf816342ddf51e44a9aead6c24be", size = 446102, upload-time = "2025-01-14T19:04:34.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cf/29fea96fd49bf72946c5dac4c43ef50f26c15e9f76edd6f15580d556aa23/pyobjc_framework_metal-11.1.tar.gz", hash = "sha256:f9fd3b7574a824632ee9b7602973da30f172d2b575dd0c0f5ef76b44cfe9f6f9", size = 446549, upload-time = "2025-06-14T20:57:54.731Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fe/083727028e63ffcf7455d10288df05696737ee74a31decdc671e32624f58/pyobjc_framework_Metal-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ac5f317d52cd7523dea2e172fbe8b03e7452b907da42a0a5e5c5ab427c5e9de", size = 57321, upload-time = "2025-01-14T18:55:39.025Z" }, - { url = "https://files.pythonhosted.org/packages/78/85/396ad46929ec6e2aa554c29a3fae2f7c7ffb2e1a3fbb9c41948d5a573dc8/pyobjc_framework_Metal-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45802d48d1a35cc66fee08539c8ca9fc6a0dc4ab700cf78a81cf5f8982ed6f5b", size = 57099, upload-time = "2025-01-14T18:55:40.091Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e8/cd0621e246dc0dc06f55c50af3002573ad19208e30f6806ec997ac587886/pyobjc_framework_metal-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:157a0052be459ffb35a3687f77a96ea87b42caf4cdd0b9f7245242b100edb4f0", size = 58066, upload-time = "2025-06-14T20:51:44.243Z" }, + { url = "https://files.pythonhosted.org/packages/4c/94/3d5a8bed000dec4a13e72dde175898b488192716b7256a05cc253c77020d/pyobjc_framework_metal-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f3aae0f9a4192a7f4f158dbee126ab5ef63a81bf9165ec63bc50c353c8d0e6f", size = 57969, upload-time = "2025-06-14T20:51:45.051Z" }, ] [[package]] name = "pyobjc-framework-metalfx" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-metal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/cf/ff9367e4737a12ebd12a17e693ec247028cf065761acc073ebefb2b2393a/pyobjc_framework_metalfx-11.0.tar.gz", hash = "sha256:2ae41991bf7a733c44fcd5b6550cedea3accaaf0f529643975d3da113c9f0caa", size = 26436, upload-time = "2025-01-14T19:04:36.161Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/20/4c839a356b534c161fb97e06589f418fc78cc5a0808362bdecf4f9a61a8d/pyobjc_framework_metalfx-11.1.tar.gz", hash = "sha256:555c1b895d4ba31be43930f45e219a5d7bb0e531d148a78b6b75b677cc588fd8", size = 27002, upload-time = "2025-06-14T20:57:55.949Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/f1/4140b63b3128cb2f12e136c4158a082ce170e4eb979bccb628768c59fd98/pyobjc_framework_MetalFX-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3a3847812d40cb6bb7a5f0e735f9f28cba83a1e1264d4dafc630ce894e98a20", size = 10308, upload-time = "2025-01-14T18:55:47.719Z" }, - { url = "https://files.pythonhosted.org/packages/c0/85/460abd4f96a7a3efd36404a480ed4d31a51f4b3ed64dc4595502a5f725c3/pyobjc_framework_MetalFX-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a37dc271513b217fcba4a99c6cd92997ee171b49b974e0a9dd1b35feb32b7109", size = 10338, upload-time = "2025-01-14T18:55:48.593Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f5/df29eeaaf053cd931fb74204a5f8827f88875a81c456b1e0fa24ea0bbcee/pyobjc_framework_metalfx-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cbfca74f437fcde89de85d14de33c2e617d3084f5fc2b4d614a700e516324f55", size = 10091, upload-time = "2025-06-14T20:51:51.084Z" }, + { url = "https://files.pythonhosted.org/packages/36/73/a8df8fa445a09fbc917a495a30b13fbcf224b5576c1e464d5ece9824a493/pyobjc_framework_metalfx-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:60e1dcdf133d2504d810c3a9ba5a02781c9d54c2112a9238de8e3ca4e8debf31", size = 10107, upload-time = "2025-06-14T20:51:51.783Z" }, ] [[package]] name = "pyobjc-framework-metalkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-metal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/27/fb3c1b10914abf2ae6682837abf76bcd8cb7af2ba613fbc55fb9d055bb95/pyobjc_framework_metalkit-11.0.tar.gz", hash = "sha256:1bbbe35c7c6a481383d32f6eaae59a1cd8084319a65c1aa343d63a257d8b4ddb", size = 44628, upload-time = "2025-01-14T19:04:36.977Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/cb/7e01bc61625c7a6fea9c9888c9ed35aa6bbc47cda2fcd02b6525757bc2b8/pyobjc_framework_metalkit-11.1.tar.gz", hash = "sha256:8811cd81ee9583b9330df4f2499a73dcc53f3359cb92767b409acaec9e4faa1e", size = 45135, upload-time = "2025-06-14T20:57:56.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/44/e7eb6746d9e1ad0ad08ab0a8ac20d264b049960363a8f28a744d1d9c319c/pyobjc_framework_MetalKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f314478a5d772d2f7b4db09957ecb63acd6e3f0cde8c18b1b6b35caa9ea7def2", size = 8598, upload-time = "2025-01-14T18:55:56.761Z" }, - { url = "https://files.pythonhosted.org/packages/a6/1c/1ae6d629065e495e8e0b7def36e1d632e461a933f616f9776a914d69b2fd/pyobjc_framework_MetalKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f2d93180e7ac5abd906e492165a72f82d308d68101eadd213bba68a4b1dc4a8", size = 8611, upload-time = "2025-01-14T18:55:58.085Z" }, + { url = "https://files.pythonhosted.org/packages/2a/eb/fd5640015fc91b16e23cafe3a84508775344cd13f621e62b9c32d1750a83/pyobjc_framework_metalkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:95abb993d17be7a9d1174701594cc040e557983d0a0e9f49b1dfa9868ef20ed6", size = 8711, upload-time = "2025-06-14T20:51:56.765Z" }, + { url = "https://files.pythonhosted.org/packages/87/0c/516b6d7a67a170b7d2316701d5288797a19dd283fcc2f73b7b78973e1392/pyobjc_framework_metalkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4854cf74fccf6ce516b49bf7cf8fc7c22da9a3743914a2f4b00f336206ad47ec", size = 8730, upload-time = "2025-06-14T20:51:57.824Z" }, ] [[package]] name = "pyobjc-framework-metalperformanceshaders" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-metal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/c2/c08996a8c6cfef09fb9e726cc99b0bf3ad0ffcef66d5c2543e6b35dd4e2e/pyobjc_framework_metalperformanceshaders-11.0.tar.gz", hash = "sha256:41179e3a11e55325153fffd84f48946d47c1dc1944677febd871a127021e056d", size = 301444, upload-time = "2025-01-14T19:04:38.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/11/5df398a158a6efe2c87ac5cae121ef2788242afe5d4302d703147b9fcd91/pyobjc_framework_metalperformanceshaders-11.1.tar.gz", hash = "sha256:8a312d090a0f51651e63d9001e6cc7c1aa04ceccf23b494cbf84b7fd3d122071", size = 302113, upload-time = "2025-06-14T20:57:57.407Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/e9/3741ac0e745e1014961f12cf967eac1d4ec5b110d3ed13fdf9dd4ce27933/pyobjc_framework_MetalPerformanceShaders-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:460a30ff31f04bbe82bf3304949171e148e3171ba0c0773dd9bfc42dad766d2e", size = 33004, upload-time = "2025-01-14T18:56:02.993Z" }, - { url = "https://files.pythonhosted.org/packages/39/b4/51434a9a897a47f6a0d1f6079725e3de4dbc75a7004275f116a2043cf80b/pyobjc_framework_MetalPerformanceShaders-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:abd4649de32aedfa45f8535d74227ba3e1411b6426f794026e8426feab43ea8e", size = 33222, upload-time = "2025-01-14T18:56:04.78Z" }, + { url = "https://files.pythonhosted.org/packages/64/ce/bbcf26f8aa94fb6edcf1a71ef23cd8df2afd4b5c2be451432211827c2ab0/pyobjc_framework_metalperformanceshaders-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:81ec1f85c55d11529008e6a0fb1329d5184620f04d89751c11bf14d7dd9798ee", size = 32650, upload-time = "2025-06-14T20:52:04.451Z" }, + { url = "https://files.pythonhosted.org/packages/89/df/f844516a54ef0fa1d047fe5fd94b63bc8b1218c09f7d4309b2a67a79708d/pyobjc_framework_metalperformanceshaders-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:06b2a4e446fe859e30f7efc7ccfbaefd443225a6ec53d949a113a6a4acc16c4c", size = 32888, upload-time = "2025-06-14T20:52:05.225Z" }, ] [[package]] name = "pyobjc-framework-metalperformanceshadersgraph" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-metalperformanceshaders" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/b8/353852c76eb437e907ca0acf8a5b5f9255e9b9ee8c0706b69b0c17498f97/pyobjc_framework_metalperformanceshadersgraph-11.0.tar.gz", hash = "sha256:33077ebbbe1aa7787de2552a83534be6c439d7f4272de17915a85fda8fd3b72d", size = 105381, upload-time = "2025-01-14T19:04:39.831Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/c3/8d98661f7eecd1f1b0d80a80961069081b88efd3a82fbbed2d7e6050c0ad/pyobjc_framework_metalperformanceshadersgraph-11.1.tar.gz", hash = "sha256:d25225aab4edc6f786b29fe3d9badc4f3e2d0caeab1054cd4f224258c1b6dbe2", size = 105098, upload-time = "2025-06-14T20:57:58.273Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/8c/3d8f1cc6cfe7f9fd73f3911bb62256fdefc4d7f5375b8be84870d8c15650/pyobjc_framework_MetalPerformanceShadersGraph-11.0-py2.py3-none-any.whl", hash = "sha256:d48ffe401fbc8273a23e908685635a51c64d4ebfb5ad32742664ab9fac6c5194", size = 6403, upload-time = "2025-01-14T18:56:10.236Z" }, - { url = "https://files.pythonhosted.org/packages/ef/26/ca0441ac11d5ecc7814b48b3af9df467ead93622f0edc67e947f1a4afe97/pyobjc_framework_MetalPerformanceShadersGraph-11.0-py3-none-any.whl", hash = "sha256:f0702a6e91b273e552283ff2782220ce08eb65325aa45ad428e0b7f3b45cf211", size = 6474, upload-time = "2025-01-14T18:56:11.479Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a1/2033cf8b0d9f059e3495a1d9a691751b242379c36dd5bcb96c8edb121c9e/pyobjc_framework_metalperformanceshadersgraph-11.1-py2.py3-none-any.whl", hash = "sha256:9b8b014e8301c2ae608a25f73bbf23c8f3f73a6f5fdbafddad509a21b84df681", size = 6461, upload-time = "2025-06-14T20:52:10.522Z" }, ] [[package]] name = "pyobjc-framework-metrickit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/82/605ad654f40ff4480ba9366ad3726da80c98e33b73f122fb91259be1ce81/pyobjc_framework_metrickit-11.0.tar.gz", hash = "sha256:ee3da403863beec181a2d6dc7b7eeb4d07e954b88bbabac58a82523b2f83fdc7", size = 40414, upload-time = "2025-01-14T19:04:41.186Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/48/8ae969a51a91864000e39c1de74627b12ff587b1dbad9406f7a30dfe71f8/pyobjc_framework_metrickit-11.1.tar.gz", hash = "sha256:a79d37575489916c35840e6a07edd958be578d3be7a3d621684d028d721f0b85", size = 40952, upload-time = "2025-06-14T20:57:58.996Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/1f/cc897b07b3ed96a26a3008f43e0deefaa60e280ac13118a2ff4224fca0d8/pyobjc_framework_MetricKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:422b6ca1f082dae864df8cc4aa0bac3829be95675b72ef63cd3ee00d30966b97", size = 7958, upload-time = "2025-01-14T18:56:13.428Z" }, - { url = "https://files.pythonhosted.org/packages/19/63/f37010479670958d3c976d007d45107c3fc53b5626586527c6310821e15a/pyobjc_framework_MetricKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b94313601bbf0181c8f75712e82646261ff0e020da5c83d25914952db53a7955", size = 7966, upload-time = "2025-01-14T18:56:14.36Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/e459511c194d25c4acd31cbdb5c118215795785840861d55dbc8bd55cf35/pyobjc_framework_metrickit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a5d2b394f7acadd17d8947d188106424f59393b45dd4a842ac3cc50935170e3e", size = 8063, upload-time = "2025-06-14T20:52:12.696Z" }, + { url = "https://files.pythonhosted.org/packages/55/d1/aea4655e7eaa9ab19da8fe78ab363270443059c8a542b8f8a071b4988b57/pyobjc_framework_metrickit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a034e6b982e915da881edef87d71b063e596511d52aef7a32c683571f364156e", size = 8081, upload-time = "2025-06-14T20:52:13.72Z" }, ] [[package]] name = "pyobjc-framework-mlcompute" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/c9/22fe4720685724ec1444c8e5cdb41d360b1434d0971fb3e43cf3e9bf51fd/pyobjc_framework_mlcompute-11.0.tar.gz", hash = "sha256:1a1ee9ab43d1824300055ff94b042a26f38f1d18f6f0aa08be1c88278e7284d9", size = 89265, upload-time = "2025-01-14T19:04:43.326Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/e6/f064dec650fb1209f41aba0c3074416cb9b975a7cf4d05d93036e3d917f0/pyobjc_framework_mlcompute-11.1.tar.gz", hash = "sha256:f6c4c3ea6a62e4e3927abf9783c40495aa8bb9a8c89def744b0822da58c2354b", size = 89021, upload-time = "2025-06-14T20:57:59.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/06/a5865c0e4db4e7289bf6b40242b7149af87d5779f34ca168df5cabf2d5a4/pyobjc_framework_MLCompute-11.0-py2.py3-none-any.whl", hash = "sha256:16ec2942af9915f931df76b42e7f42348109b599faef955f5bea540735f87677", size = 6729, upload-time = "2025-01-14T18:54:55.927Z" }, - { url = "https://files.pythonhosted.org/packages/b5/15/3c69df5b5b99cea4a573e1d0e3c0b607cfe4ea1404ea1fe3a302361eb452/pyobjc_framework_MLCompute-11.0-py3-none-any.whl", hash = "sha256:bcdf94fe060fb034aed41db84af1cfcdbf3925e69b2b11df89d4546fac6cf0bf", size = 6799, upload-time = "2025-01-14T18:54:56.893Z" }, + { url = "https://files.pythonhosted.org/packages/23/cc/f47a4ac2d1a792b82206fdab58cc61b3aae15e694803ea2c81f3dfc16d9d/pyobjc_framework_mlcompute-11.1-py2.py3-none-any.whl", hash = "sha256:975150725e919f8d3d33f830898f3cd2fd19a440999faab320609487f4eae19d", size = 6778, upload-time = "2025-06-14T20:52:19.844Z" }, ] [[package]] name = "pyobjc-framework-modelio" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/7c/b75b84d41e7854ffe9c9a42846f8105227a5fd0b02b690b4a75018b2caa3/pyobjc_framework_modelio-11.0.tar.gz", hash = "sha256:c875eb6ff7f94d18362a00faaa3016ae0c28140326338d18aa03c0b62f1c6b9d", size = 122652, upload-time = "2025-01-14T19:04:44.263Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/27/140bf75706332729de252cc4141e8c8afe16a0e9e5818b5a23155aa3473c/pyobjc_framework_modelio-11.1.tar.gz", hash = "sha256:fad0fa2c09d468ac7e49848e144f7bbce6826f2178b3120add8960a83e5bfcb7", size = 123203, upload-time = "2025-06-14T20:58:01.035Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/98/a30e8df5624c7929dfcd9748bf859929e8aa2c7d836efe5888dafc05f729/pyobjc_framework_ModelIO-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c126318b878ffb31c39b0c7c91ca20a3b46c14c18f000e3bfb854e4541fe0147", size = 20715, upload-time = "2025-01-14T18:56:22.163Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f8/bb4bc635eb16331c20731cae2e495645d0d10e25962451631eb9085a3f85/pyobjc_framework_ModelIO-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a7357f07b77f3ab0a8107d827acdbc3e1fd458ce396335c057930b6a3f225a93", size = 20715, upload-time = "2025-01-14T18:56:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/6c/66/8109e52c7d97a108d4852a2032c9d7a7ecd27c6085bd7b2920b2ab575df4/pyobjc_framework_modelio-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4365fb96eb42b71c12efdfa2ff9d44755d5c292b8d1c78b947833d84271e359f", size = 20142, upload-time = "2025-06-14T20:52:21.582Z" }, + { url = "https://files.pythonhosted.org/packages/18/84/5f223b82894777388ef1aa09579d9c044044877a72075213741c97adc901/pyobjc_framework_modelio-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5d5e11389bde0852490b2a37896aaf9eb674b2a3586f2c572f9101cecb7bc576", size = 20172, upload-time = "2025-06-14T20:52:22.327Z" }, ] [[package]] name = "pyobjc-framework-multipeerconnectivity" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/80/4137cb9751aa3846c4954b3e61f948aae17afeb6851e01194aa50683caef/pyobjc_framework_multipeerconnectivity-11.0.tar.gz", hash = "sha256:8278a3483c0b6b88a8888ca76c46fd85808f9df56d45708cbc4e4182a5565cd3", size = 25534, upload-time = "2025-01-14T19:04:45.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/99/75bf6170e282d9e546b353b65af7859de8b1b27ddc431fc4afbf15423d01/pyobjc_framework_multipeerconnectivity-11.1.tar.gz", hash = "sha256:a3dacca5e6e2f1960dd2d1107d98399ff81ecf54a9852baa8ec8767dbfdbf54b", size = 26149, upload-time = "2025-06-14T20:58:01.793Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/d2/a4144f966cbe998f8da46b936783561bcd3e7e84b8f2dc45eb49ee3f6f21/pyobjc_framework_MultipeerConnectivity-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e338b22f5b0fcb398e316552398c252bedfc3375c058340861eb205e3cdf994e", size = 12423, upload-time = "2025-01-14T18:56:30.132Z" }, - { url = "https://files.pythonhosted.org/packages/7b/50/ac9213aca34d30993a36525c23d19ba5a568d3ea4e31e3bc2a6940ddafde/pyobjc_framework_MultipeerConnectivity-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:66bef15f5e5afd6b966cdadf2162082b0171f4a45af6d2cb2644f38431011911", size = 12447, upload-time = "2025-01-14T18:56:31.04Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/a3fc2514879a39673202f7ea5e835135255c5e510d30c58a43239ec1d9e0/pyobjc_framework_multipeerconnectivity-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b3c9d4d36e0c142b4ce91033740ed5bca19fe7ec96870d90610d2942ecd3cd39", size = 11955, upload-time = "2025-06-14T20:52:28.392Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fe/5c29c227f6ed81147ec6ec3e681fc680a7ffe0360f96901371435ea68570/pyobjc_framework_multipeerconnectivity-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:970031deb3dbf8da1fcb04e785d4bd2eeedae8f6677db92881df6d92b05c31d6", size = 11981, upload-time = "2025-06-14T20:52:29.406Z" }, ] [[package]] name = "pyobjc-framework-naturallanguage" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/64/63e97635fa637384bc8c980796573dc7a9e7074a6866aef073b1faf3e11d/pyobjc_framework_naturallanguage-11.0.tar.gz", hash = "sha256:4c9471fa2c48a8fd4899de4406823e66cb0292dbba7b471622017f3647d53fa4", size = 46385, upload-time = "2025-01-14T19:04:46.185Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/e9/5352fbf09c5d5360405dea49fb77e53ed55acd572a94ce9a0d05f64d2b70/pyobjc_framework_naturallanguage-11.1.tar.gz", hash = "sha256:ab1fc711713aa29c32719774fc623bf2d32168aed21883970d4896e901ff4b41", size = 46120, upload-time = "2025-06-14T20:58:02.808Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/72/2246c0a6dc2d087951a626157f52c81cf88fe28393994163e9572fd1eb61/pyobjc_framework_NaturalLanguage-11.0-py2.py3-none-any.whl", hash = "sha256:0744a2871690dcc9ec9e7169023b492abdde63ef97abde46013c01477b4d047c", size = 5250, upload-time = "2025-01-14T18:56:34.675Z" }, - { url = "https://files.pythonhosted.org/packages/3a/49/f5faf3fab0f1ffb21882115878f1e5023257239aa576d6c01c31e42dd1da/pyobjc_framework_NaturalLanguage-11.0-py3-none-any.whl", hash = "sha256:7c021b270fda5469b56b9804e860cf5a80a485b817fc5fd3bb002383b2982d94", size = 5321, upload-time = "2025-01-14T18:56:36.377Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/de86665d48737c74756b016c0f3bf93c99ca4151b48b14e2fbe7233283f8/pyobjc_framework_naturallanguage-11.1-py2.py3-none-any.whl", hash = "sha256:65a780273d2cdd12a3fa304e9c9ad822cb71facd9281f1b35a71640c53826f7c", size = 5306, upload-time = "2025-06-14T20:52:34.024Z" }, ] [[package]] name = "pyobjc-framework-netfs" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/29/eb569870b52c7581104ed2806cae2d425d60b5ab304128cd58155d5b567f/pyobjc_framework_netfs-11.0.tar.gz", hash = "sha256:3de5f627a62addf4aab8a4d2d07213e9b2b6c8adbe6cc4c332ee868075785a6a", size = 16173, upload-time = "2025-01-14T19:04:47.11Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/5d/d68cc59a1c1ea61f227ed58e7b185a444d560655320b53ced155076f5b78/pyobjc_framework_netfs-11.1.tar.gz", hash = "sha256:9c49f050c8171dc37e54d05dd12a63979c8b6b565c10f05092923a2250446f50", size = 15910, upload-time = "2025-06-14T20:58:03.811Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/e7/4be35bc2adbebffb5ac7ede2b8459432194a82bd8f325af12b77b7c26248/pyobjc_framework_NetFS-11.0-py2.py3-none-any.whl", hash = "sha256:11e06da73a1d590b8462f3a1412604758d49b5e04d134b6e991282453b76abb8", size = 4088, upload-time = "2025-01-14T18:56:37.646Z" }, - { url = "https://files.pythonhosted.org/packages/fe/83/b7c8dfaee82c0312af25c2b31621505ce19f01fab7bb55eec69c0b4d24ad/pyobjc_framework_NetFS-11.0-py3-none-any.whl", hash = "sha256:9b69a36e3a6782ce37cd3140c584dd7d5c96f7355662d004a2927583b112b4dd", size = 4162, upload-time = "2025-01-14T18:56:38.682Z" }, + { url = "https://files.pythonhosted.org/packages/77/cc/199b06f214f8a2db26eb47e3ab7015a306597a1bca25dcb4d14ddc65bd4a/pyobjc_framework_netfs-11.1-py2.py3-none-any.whl", hash = "sha256:f202e8e0c2e73516d3eac7a43b1c66f9911cdbb37ea32750ed197d82162c994a", size = 4143, upload-time = "2025-06-14T20:52:35.428Z" }, ] [[package]] name = "pyobjc-framework-network" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/8e/18e55aff83549e041484d2ee94dd91b29cec9de40508e7fe9c4afec110a7/pyobjc_framework_network-11.0.tar.gz", hash = "sha256:d4dcc02773d7d642a385c7f0d951aeb7361277446c912a49230cddab60a65ab8", size = 124160, upload-time = "2025-01-14T19:04:50.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/ee/5ea93e48eca341b274027e1532bd8629fd55d609cd9c39c2c3acf26158c3/pyobjc_framework_network-11.1.tar.gz", hash = "sha256:f6df7a58a1279bbc976fd7e2efe813afbbb18427df40463e6e2ee28fba07d2df", size = 124670, upload-time = "2025-06-14T20:58:05.491Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/b5/16800524e6d8d99467f53dbafa661abb1405d08d50def7edb933504197a3/pyobjc_framework_Network-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6fc797537690a241b555475923bcee28824efacd501e235457daeb4496b4b700", size = 19507, upload-time = "2025-01-14T18:56:41.544Z" }, - { url = "https://files.pythonhosted.org/packages/36/7c/a5966976564e8e71c0e66bf68e9282c279ad0c3ce81be61fa20ca8e0ca2e/pyobjc_framework_Network-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0b9bb4a0cbd01cc4acb120ce313662763bca0c5ef11c01a0a0cae64c80b120c5", size = 19532, upload-time = "2025-01-14T18:56:42.499Z" }, + { url = "https://files.pythonhosted.org/packages/17/e9/a54f32daa0365bf000b739fc386d4783432273a9075337aa57a3808af65d/pyobjc_framework_network-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e56691507584c09cdb50f1cd69b5f57b42fd55c396e8c34fab8c5b81b44d36ed", size = 19499, upload-time = "2025-06-14T20:52:37.158Z" }, + { url = "https://files.pythonhosted.org/packages/15/c2/3c6626fdb3616fde2c173d313d15caea22d141abcc2fbf3b615f8555abe3/pyobjc_framework_network-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8cdc9be8ec3b0ae95e5c649e4bbcdf502cffd357dacc566223be707bdd5ac271", size = 19513, upload-time = "2025-06-14T20:52:38.423Z" }, ] [[package]] name = "pyobjc-framework-networkextension" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/90/97dcfac5895b07e891adf634c3a074b68992d132ccfab386c186ac1a598c/pyobjc_framework_networkextension-11.0.tar.gz", hash = "sha256:5ba2254e2c13010b6c4f1e2948047d95eff86bfddfc77716747718fa3a8cb1af", size = 188551, upload-time = "2025-01-14T19:04:51.352Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/30/d1eee738d702bbca78effdaa346a2b05359ab8a96d961b7cb44838e236ca/pyobjc_framework_networkextension-11.1.tar.gz", hash = "sha256:2b74b430ca651293e5aa90a1e7571b200d0acbf42803af87306ac8a1c70b0d4b", size = 217252, upload-time = "2025-06-14T20:58:06.311Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/a4/120aba6e1ccf473d7294c200687f500b096947fec58d94dc772b1a444ecc/pyobjc_framework_NetworkExtension-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4bba4f338748c8ad2cb4320c4dd64b64772a863c6b6f991c2636b2a2f4cb839a", size = 13945, upload-time = "2025-01-14T18:56:53.477Z" }, - { url = "https://files.pythonhosted.org/packages/d1/0f/f7039d2bae0dcd63f66aff008613860499b6014dbd272726026f6c4c768d/pyobjc_framework_NetworkExtension-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:abf63433992ff1830f42cb813d1575473f0034ca6f62827f43bb2b33cc31e095", size = 13960, upload-time = "2025-01-14T18:56:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/76/d7/b10aa191d37900ade78f1b7806d17ff29fa95f40ce7aeecce6f15ec94ac9/pyobjc_framework_networkextension-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:55e5ca70c81a864896b603cfcabf4c065783f64395460d16fe16db2bf0866d60", size = 14101, upload-time = "2025-06-14T20:52:44.527Z" }, + { url = "https://files.pythonhosted.org/packages/b6/26/526cd9f63e390e9c2153c41dc0982231b0b1ca88865deb538b77e1c3513d/pyobjc_framework_networkextension-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:853458aae8b43634461f6c44759750e2dc784c9aba561f9468ab14529b5a7fbe", size = 14114, upload-time = "2025-06-14T20:52:45.274Z" }, ] [[package]] name = "pyobjc-framework-notificationcenter" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/d0/f0a602e01173531a2b639e283a092cf1f307fd873abd2ed590b9c4122337/pyobjc_framework_notificationcenter-11.0.tar.gz", hash = "sha256:f878b318c693d63d6b8bd1c3e2ad4f8097b22872f18f40142e394d84f1ead9f6", size = 22844, upload-time = "2025-01-14T19:04:52.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4a/d3529b9bd7aae2c89d258ebc234673c5435e217a5136abd8c0aba37b916b/pyobjc_framework_notificationcenter-11.1.tar.gz", hash = "sha256:0b938053f2d6b1cea9db79313639d7eb9ddd5b2a5436a346be0887e75101e717", size = 23389, upload-time = "2025-06-14T20:58:07.136Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/f2/22f04062b772e2f47ee2d54eac3f80c5aef727ec468ef5ab9a3272dd2a73/pyobjc_framework_NotificationCenter-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:075853f3e36eb4377182589e552226b2207a575035d7e128055cfde9dcad84b7", size = 9684, upload-time = "2025-01-14T18:57:02.779Z" }, - { url = "https://files.pythonhosted.org/packages/16/22/531c2aab1639ab13aeaf3ac324afa102515b8d5eb860cb1a566018d98058/pyobjc_framework_NotificationCenter-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:093e50badfbc78edf088f9241cddba7516a58188d401f299e361f1ec85e93fce", size = 9707, upload-time = "2025-01-14T18:57:03.659Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/3beb825e2b80de45b90e7cd510ad52890ac4a5a4de88cd9a5291235519fb/pyobjc_framework_notificationcenter-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d44413818e7fa3662f784cdcf0730c86676dd7333b7d24a7da13d4ffcde491b", size = 9859, upload-time = "2025-06-14T20:52:51.744Z" }, + { url = "https://files.pythonhosted.org/packages/6d/92/cd00fe5e54a191fb77611fe728a8c8a0a6edb229857d32f27806582406ca/pyobjc_framework_notificationcenter-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:65fc67374a471890245c7a1d60cf67dcf160075a9c048a5d89608a8290f33b03", size = 9880, upload-time = "2025-06-14T20:52:52.406Z" }, ] [[package]] name = "pyobjc-framework-opendirectory" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/cf/ba0cf807758acdc6a19e4787fdcda2eb59034aa22c4203d04fd49b276981/pyobjc_framework_opendirectory-11.0.tar.gz", hash = "sha256:0c82594f4f0bcf2318c4641527f9243962d7b03e67d4f3fb111b899a299fc7eb", size = 189165, upload-time = "2025-01-14T19:04:53.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/02/ac56c56fdfbc24cdf87f4a624f81bbe2e371d0983529b211a18c6170e932/pyobjc_framework_opendirectory-11.1.tar.gz", hash = "sha256:319ac3424ed0350be458b78148914468a8fc13a069d62e7869e3079108e4f118", size = 188880, upload-time = "2025-06-14T20:58:08.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/0a/e5a03c46a5873db83fb89ea829e4a0c02fb3f56f3639a6053e72854f435b/pyobjc_framework_OpenDirectory-11.0-py2.py3-none-any.whl", hash = "sha256:8a0feeda5a7f34b25b72c71cd1e4dd57b636cc4103248ff91bcb8571d4915eb4", size = 11747, upload-time = "2025-01-14T18:57:17.445Z" }, - { url = "https://files.pythonhosted.org/packages/da/fd/be3815a19978ab2a3abe9563a031195b40647077fcebbee86232af260176/pyobjc_framework_OpenDirectory-11.0-py3-none-any.whl", hash = "sha256:bfac495de433a62e3934619e2f5d2254177f960b7d4e905ed4ef359127e23b24", size = 11816, upload-time = "2025-01-14T18:57:18.486Z" }, + { url = "https://files.pythonhosted.org/packages/06/56/f0f5b7222d5030192c44010ab7260681e349efea2f1b1b9f116ba1951d6d/pyobjc_framework_opendirectory-11.1-py2.py3-none-any.whl", hash = "sha256:bb4219b0d98dff4a952c50a79b1855ce74e1defd0d241f3013def5b09256fd7b", size = 11829, upload-time = "2025-06-14T20:52:56.715Z" }, ] [[package]] name = "pyobjc-framework-osakit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/4a/e49680f7f3ab9c0632ed9be76a0a59299e7fd797335690b3da4d117f2d7b/pyobjc_framework_osakit-11.0.tar.gz", hash = "sha256:77ac18e2660133a9eeb01c76ad3df3b4b36fd29005fc36bca00f57cca121aac3", size = 22535, upload-time = "2025-01-14T19:04:54.753Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/22/f9cdfb5de255b335f99e61a3284be7cb1552a43ed1dfe7c22cc868c23819/pyobjc_framework_osakit-11.1.tar.gz", hash = "sha256:920987da78b67578367c315d208f87e8fab01dd35825d72242909f29fb43c820", size = 22290, upload-time = "2025-06-14T20:58:09.103Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/f6/1dcff2f76280946368ee75ab39c92e261a851656c5979a50513563d08cf0/pyobjc_framework_OSAKit-11.0-py2.py3-none-any.whl", hash = "sha256:3183414e345af83a2187b00356130909a7c2a41b2227dc579b662737300c3ba4", size = 4094, upload-time = "2025-01-14T18:57:08.639Z" }, - { url = "https://files.pythonhosted.org/packages/17/75/745985429f0ff4776ffb8ba261199e11f4d6977b1814ad2b39084f83bad5/pyobjc_framework_OSAKit-11.0-py3-none-any.whl", hash = "sha256:79150c47d2aeffc72fb6551060518ce472275edbad3b56aef5923a6086371c28", size = 4162, upload-time = "2025-01-14T18:57:09.71Z" }, + { url = "https://files.pythonhosted.org/packages/14/65/c6531ce0792d5035d87f054b0ccf22e453328fda2e68e11a7f70486da23a/pyobjc_framework_osakit-11.1-py2.py3-none-any.whl", hash = "sha256:1b0c0cc537ffb8a8365ef9a8b46f717a7cc2906414b6a3983777a6c0e4d53d5a", size = 4143, upload-time = "2025-06-14T20:52:57.555Z" }, ] [[package]] name = "pyobjc-framework-oslog" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, @@ -3625,585 +3570,583 @@ dependencies = [ { name = "pyobjc-framework-coremedia" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/93/0a72353d0212a815bd5e43aec528ce7b28b71d461d26e5fa3882ff96ffa3/pyobjc_framework_oslog-11.0.tar.gz", hash = "sha256:9d29eb7c89a41d7c702dffb6e2e338a2d5219387c8dae22b67754ddf9e2fcb3f", size = 24151, upload-time = "2025-01-14T19:04:55.587Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/93/3feb7f6150b50165524750a424f5434448392123420cb4673db766c3f54a/pyobjc_framework_oslog-11.1.tar.gz", hash = "sha256:b2af409617e6b68fa1f1467c5a5679ebf59afd0cdc4b4528e1616059959a7979", size = 24689, upload-time = "2025-06-14T20:58:09.739Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/54/6b507a18d0adadf8b707be9616bc9bab157963b81fa3c9928a0148d3bfd8/pyobjc_framework_OSLog-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0131851fca9b741f290ffa727dd30328dd8526b87c8cef623b79239bed99187", size = 7694, upload-time = "2025-01-14T18:57:12.713Z" }, - { url = "https://files.pythonhosted.org/packages/d1/79/81e64a55023f458aa5d99d10671fd9bcc6c0dcf8339768152fbc28c92cef/pyobjc_framework_OSLog-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:17d8b49113a476372b24ac8e544d88f6d12f878f1081dd611ab203c4484f2039", size = 7720, upload-time = "2025-01-14T18:57:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/66/7a/2db26fc24e16c84312a0de432bab16ca586223fd6c5ba08e49c192ae95f6/pyobjc_framework_oslog-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5dab25ef1cde4237cd2957c1f61c2888968e924304f7b9d9699eceeb330e9817", size = 7793, upload-time = "2025-06-14T20:52:59.132Z" }, + { url = "https://files.pythonhosted.org/packages/40/da/fd3bd62899cd679743056aa2c28bc821c2688682a17ddde1a08d6d9d67fc/pyobjc_framework_oslog-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7ae29c31ce51c476d3a37ca303465dd8bdfa98df2f6f951cf14c497e984a1ba9", size = 7799, upload-time = "2025-06-14T20:52:59.935Z" }, ] [[package]] name = "pyobjc-framework-passkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/f8/ebb2bc840f87292a4f60080463ee698ca08516cc958364741dfff2858b33/pyobjc_framework_passkit-11.0.tar.gz", hash = "sha256:2044d9d634dd98b7b624ee09487b27e5f26a7729f6689abba23a4a011febe19c", size = 120495, upload-time = "2025-01-14T19:04:57.689Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/05/063db500e7df70e39cbb5518a5a03c2acc06a1ca90b057061daea00129f3/pyobjc_framework_passkit-11.1.tar.gz", hash = "sha256:d2408b58960fca66607b483353c1ffbd751ef0bef394a1853ec414a34029566f", size = 144859, upload-time = "2025-06-14T20:58:10.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/72/d7dae8f5a1c5b12d9cf404a71a82fd5a638bc4de2d1099bf838aee1026f0/pyobjc_framework_PassKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:710372134c3adedb9017bfc2fbc592ef0e94ae916145b58e57234239bf903b90", size = 14354, upload-time = "2025-01-14T18:57:24.436Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b1/5ee2f5581877241a4fc2db4ab4a33d595a918bde1b4a59796240e2b2244b/pyobjc_framework_PassKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe0144177f7feb96577bea53841d9b9b3f63185735a1bf1b36368ab189fd6282", size = 14391, upload-time = "2025-01-14T18:57:26.67Z" }, + { url = "https://files.pythonhosted.org/packages/80/18/343eb846e62704fbd64e178e0cbf75b121955c1973bf51ddd0871a42910a/pyobjc_framework_passkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:67b7b1ee9454919c073c2cba7bdba444a766a4e1dd15a5e906f4fa0c61525347", size = 13949, upload-time = "2025-06-14T20:53:04.98Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/9e52213e0c0100079e4ef397cf4fd5ba8939fa4de19339755d1a373407a8/pyobjc_framework_passkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:779eaea4e1931cfda4c8701e1111307b14bf9067b359a319fc992b6848a86932", size = 13959, upload-time = "2025-06-14T20:53:05.694Z" }, ] [[package]] name = "pyobjc-framework-pencilkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/8d/1e97cd72b776e5e1294cbda84325b364702617dd435d32448dcc0a80bd93/pyobjc_framework_pencilkit-11.0.tar.gz", hash = "sha256:9598c28e83f5b7f091592cc1af2b16f7ae94cf00045d8d14ed2c17cb9e4ffd50", size = 22812, upload-time = "2025-01-14T19:04:58.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/d0/bbbe9dadcfc37e33a63d43b381a8d9a64eca27559df38efb74d524fa6260/pyobjc_framework_pencilkit-11.1.tar.gz", hash = "sha256:9c173e0fe70179feadc3558de113a8baad61b584fe70789b263af202bfa4c6be", size = 22570, upload-time = "2025-06-14T20:58:11.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/5b/24fb83a97648eaa0d231df7908532dff7b36d5f516d55c92ed9ae07c4e1b/pyobjc_framework_PencilKit-11.0-py2.py3-none-any.whl", hash = "sha256:22cbb6ed2504be4c8d631c4711b00fae48ef731c10c69861b4de1e4fcdc19279", size = 3970, upload-time = "2025-01-14T18:57:30.597Z" }, - { url = "https://files.pythonhosted.org/packages/08/fd/89a005c86b06137837952838d976ce6e39b31082392d78c382d44e03944d/pyobjc_framework_PencilKit-11.0-py3-none-any.whl", hash = "sha256:a4e606c5b69e6adb80ef30fc95fe0095971735d12ab6fc4fe4d982e4c8a3881a", size = 4045, upload-time = "2025-01-14T18:57:31.87Z" }, + { url = "https://files.pythonhosted.org/packages/a3/f6/59ffc3f26ea9cfda4d40409f9afc2a38e5c0c6a68a3a8c9202e8b98b03b1/pyobjc_framework_pencilkit-11.1-py2.py3-none-any.whl", hash = "sha256:b7824907bbcf28812f588dda730e78f662313baf40befd485c6f2fcb49018019", size = 4026, upload-time = "2025-06-14T20:53:10.449Z" }, ] [[package]] name = "pyobjc-framework-phase" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-avfoundation" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/a2/65182dcb44fceb2173f4134d6cd4325dfd0731225b621aa2027d2a03d043/pyobjc_framework_phase-11.0.tar.gz", hash = "sha256:e06a0f8308ae4f3731f88b3e1239b7bdfdda3eef97023e3ce972e2f386451d80", size = 59214, upload-time = "2025-01-14T19:04:59.461Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/d2/e9384b5b3fbcc79e8176cb39fcdd48b77f60cd1cb64f9ee4353762b037dc/pyobjc_framework_phase-11.1.tar.gz", hash = "sha256:a940d81ac5c393ae3da94144cf40af33932e0a9731244e2cfd5c9c8eb851e3fc", size = 58986, upload-time = "2025-06-14T20:58:12.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/97/efb9d770ba05d285384b0c121e9e911929893356da1944a0bb03ea0df0f2/pyobjc_framework_PHASE-11.0-py2.py3-none-any.whl", hash = "sha256:d3e41c2b2fdf4b2ce39f558a08762c6864449ff87b618e42747777ad3f821323", size = 6777, upload-time = "2025-01-14T18:57:20.135Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/03420927e4243d0ef8e3e8aa1ca511b5638743d7ec319a570a472a50d60f/pyobjc_framework_PHASE-11.0-py3-none-any.whl", hash = "sha256:78c0600477ea294304b51f8284a2fb299be284c33ae2c135e1c7cd26fdf4def4", size = 6846, upload-time = "2025-01-14T18:57:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9e/55782f02b3bfb58f030b062176e8b0dba5f8fbd6e50d27a687f559c4179d/pyobjc_framework_phase-11.1-py2.py3-none-any.whl", hash = "sha256:cfa61f9c6c004161913946501538258aed48c448b886adbf9ed035957d93fa15", size = 6822, upload-time = "2025-06-14T20:53:11.618Z" }, ] [[package]] name = "pyobjc-framework-photos" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/c3/fc755c1f8f411433d7ba2e92f3fe3e7b417e9629675ad6baf94ac8b01e64/pyobjc_framework_photos-11.0.tar.gz", hash = "sha256:cfdfdefb0d560b091425227d5c0e24a40b445b5251ff4d37bd326cd8626b80cd", size = 92122, upload-time = "2025-01-14T19:05:01.804Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/b0/576652ecd05c26026ab4e75e0d81466edd570d060ce7df3d6bd812eb90d0/pyobjc_framework_photos-11.1.tar.gz", hash = "sha256:c8c3b25b14a2305047f72c7c081ff3655b3d051f7ed531476c03246798f8156d", size = 92569, upload-time = "2025-06-14T20:58:12.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/27/62e5833b9629121b4b6ea8f2b2aa295cf6b719dc6316387f77ec0bd41d77/pyobjc_framework_Photos-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:71bf849888713e4a00eb44074c5000ed081c905ba35b3a55ee84c6367ce60ce8", size = 12085, upload-time = "2025-01-14T18:57:34.615Z" }, - { url = "https://files.pythonhosted.org/packages/b9/6e/54108271ea34b0fc51bf8d0bf677788e4d39a1e29ad481f8c78c100f3159/pyobjc_framework_Photos-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ea630c3abf4620b022f23167ef5f3d6b157b38697d7ffc5df0fc507e95bed655", size = 12107, upload-time = "2025-01-14T18:57:35.66Z" }, + { url = "https://files.pythonhosted.org/packages/df/25/ec3b0234d20948816791399e580f6dd83c0d50a24219c954708f755742c4/pyobjc_framework_photos-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:959dfc82f20513366b85cd37d8541bb0a6ab4f3bfa2f8094e9758a5245032d67", size = 12198, upload-time = "2025-06-14T20:53:13.563Z" }, + { url = "https://files.pythonhosted.org/packages/fa/24/2400e6b738d3ed622c61a7cc6604eec769f398071a1eb6a16dfdf3a9ceea/pyobjc_framework_photos-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8dbfffd29cfa63a8396ede0030785c15a5bc36065d3dd98fc6176a59e7abb3d3", size = 12224, upload-time = "2025-06-14T20:53:14.793Z" }, ] [[package]] name = "pyobjc-framework-photosui" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/2c/70ac99fb2b7ba14d220c78cf6401c0c7a47992269f85f699220a6a2cff09/pyobjc_framework_photosui-11.0.tar.gz", hash = "sha256:3c65342e31f6109d8229992b2712b29cab1021475969b55f4f215dd97e2a99db", size = 47898, upload-time = "2025-01-14T19:05:02.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/bb/e6de720efde2e9718677c95c6ae3f97047be437cda7a0f050cd1d6d2a434/pyobjc_framework_photosui-11.1.tar.gz", hash = "sha256:1c7ffab4860ce3e2b50feeed4f1d84488a9e38546db0bec09484d8d141c650df", size = 48443, upload-time = "2025-06-14T20:58:13.626Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/ec/9574692e2852d546b28bac853b2b0584c4d4f093a4befac0e105789ee9f6/pyobjc_framework_PhotosUI-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b3865d2cc4fad4d34255941fe93ce504b9d2c7a7043bd0f4c715da9f4af1cf1", size = 12165, upload-time = "2025-01-14T18:57:44.048Z" }, - { url = "https://files.pythonhosted.org/packages/90/a9/85d70fe9eee0d15a0615a3f7b2ef92120c32614e350286d347d733fcf1d0/pyobjc_framework_PhotosUI-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:66826184121cd15415750d801160721adad80b53cbb315192522229b17252ebb", size = 12176, upload-time = "2025-01-14T18:57:44.993Z" }, + { url = "https://files.pythonhosted.org/packages/af/c1/3d67c2af53fe91feb6f64dbc501bbcfd5d325b7f0f0ffffd5d033334cb03/pyobjc_framework_photosui-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d93722aeb8c134569035fd7e6632d0247e1bcb18c3cc4e0a288664218f241b85", size = 11667, upload-time = "2025-06-14T20:53:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c1/a5c84c1695e7a066743d63d10b219d94f3c07d706871682e42f7db389f5c/pyobjc_framework_photosui-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b2f278f569dfd596a32468351411518a651d12cb91e60620094e852c525a5f10", size = 11682, upload-time = "2025-06-14T20:53:21.162Z" }, ] [[package]] name = "pyobjc-framework-preferencepanes" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/01/81cc46e0a92d15f2b664b2efdcc8fd310acac570c9f63a99d446e0489784/pyobjc_framework_preferencepanes-11.0.tar.gz", hash = "sha256:ee000c351befeb81f4fa678ada85695ca4af07933b6bd9b1947164e16dd0b3e5", size = 26419, upload-time = "2025-01-14T19:05:03.787Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/ac/9324602daf9916308ebf1935b8a4b91c93b9ae993dcd0da731c0619c2836/pyobjc_framework_preferencepanes-11.1.tar.gz", hash = "sha256:6e4a55195ec9fc921e0eaad6b3038d0ab91f0bb2f39206aa6fccd24b14a0f1d8", size = 26212, upload-time = "2025-06-14T20:58:14.361Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/f7/5d0d9b94563ef06fe0a9c15ba2b77922b73bcc4b6630c487936edf382e20/pyobjc_framework_PreferencePanes-11.0-py2.py3-none-any.whl", hash = "sha256:2143851549430d6bb951adae44cb65c1986662ac7c8cbe15891ed194cbe283a2", size = 4706, upload-time = "2025-01-14T18:57:51.425Z" }, - { url = "https://files.pythonhosted.org/packages/9b/0e/76d694eea953b39318249ae24c956c3e115d8222343fb01f0186f7ca0043/pyobjc_framework_PreferencePanes-11.0-py3-none-any.whl", hash = "sha256:9f1287716374338fa99445ca13dfcc6c9be5597c8a5ce06680a8ca245b4e0acc", size = 4772, upload-time = "2025-01-14T18:57:52.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/51/75c7e32272241f706ce8168e04a32be02c4b0c244358330f730fc85695c3/pyobjc_framework_preferencepanes-11.1-py2.py3-none-any.whl", hash = "sha256:6ee5f5a7eb294e03ea3bac522ac4b69e6dc83ceceff627a0a2d289afe1e01ad9", size = 4786, upload-time = "2025-06-14T20:53:25.603Z" }, ] [[package]] name = "pyobjc-framework-pushkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/ab/7fe55ce5b32c434142be026ec27b1801a2d4694b159b502f9ecd568eebf2/pyobjc_framework_pushkit-11.0.tar.gz", hash = "sha256:df9854ed4065c50022863b3c11c2a21c4279b36c2b5c8f08b834174aacb44e81", size = 20816, upload-time = "2025-01-14T19:05:05.468Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/f0/92d0eb26bf8af8ebf6b5b88df77e70b807de11f01af0162e0a429fcfb892/pyobjc_framework_pushkit-11.1.tar.gz", hash = "sha256:540769a4aadc3c9f08beca8496fe305372501eb28fdbca078db904a07b8e10f4", size = 21362, upload-time = "2025-06-14T20:58:15.642Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/5f/de178da22fa628cd88f599fea2a70b7d1d9ebc65576307df0bf29822a347/pyobjc_framework_PushKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0185cebcc5aad73aae50804c7a2412da6275717b8f872b830d71c484efcdea7a", size = 8010, upload-time = "2025-01-14T18:57:59.042Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a5/60f93031302aba7cdff28728b8141b58c3bd5c12f4a6cef5796a8cc2e666/pyobjc_framework_PushKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:43bd1ed31664982e4d8397a7e07e58a7deb85bf9c9866ea966fd7ca25796014c", size = 8032, upload-time = "2025-01-14T18:57:59.973Z" }, + { url = "https://files.pythonhosted.org/packages/d9/dc/415d6d7e3ed04d8b2f8dc6d458e7c6db3f503737b092d71b4856bf1607f7/pyobjc_framework_pushkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5e2f08b667035df6b11a0a26f038610df1eebbedf9f3f111c241b5afaaf7c5fd", size = 8149, upload-time = "2025-06-14T20:53:28.096Z" }, + { url = "https://files.pythonhosted.org/packages/31/65/260014c5d13c54bd359221b0a890cbffdb99eecff3703f253cf648e45036/pyobjc_framework_pushkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:21993b7e9127b05575a954faa68e85301c6a4c04e34e38aff9050f67a05c562a", size = 8174, upload-time = "2025-06-14T20:53:28.805Z" }, ] [[package]] name = "pyobjc-framework-quartz" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/ad/f00f3f53387c23bbf4e0bb1410e11978cbf87c82fa6baff0ee86f74c5fb6/pyobjc_framework_quartz-11.0.tar.gz", hash = "sha256:3205bf7795fb9ae34747f701486b3db6dfac71924894d1f372977c4d70c3c619", size = 3952463, upload-time = "2025-01-14T19:05:07.931Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/ac/6308fec6c9ffeda9942fef72724f4094c6df4933560f512e63eac37ebd30/pyobjc_framework_quartz-11.1.tar.gz", hash = "sha256:a57f35ccfc22ad48c87c5932818e583777ff7276605fef6afad0ac0741169f75", size = 3953275, upload-time = "2025-06-14T20:58:17.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/6a/68957c8c5e8f0128d4d419728bac397d48fa7ad7a66e82b70e64d129ffca/pyobjc_framework_Quartz-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d251696bfd8e8ef72fbc90eb29fec95cb9d1cc409008a183d5cc3246130ae8c2", size = 212349, upload-time = "2025-01-14T18:58:08.963Z" }, - { url = "https://files.pythonhosted.org/packages/60/5d/df827b78dcb5140652ad08af8038c9ddd7e01e6bdf84462bfee644e6e661/pyobjc_framework_Quartz-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cb4a9f2d9d580ea15e25e6b270f47681afb5689cafc9e25712445ce715bcd18e", size = 212061, upload-time = "2025-01-14T18:58:10.2Z" }, + { url = "https://files.pythonhosted.org/packages/77/cb/38172fdb350b3f47e18d87c5760e50f4efbb4da6308182b5e1310ff0cde4/pyobjc_framework_quartz-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2d501fe95ef15d8acf587cb7dc4ab4be3c5a84e2252017da8dbb7df1bbe7a72a", size = 215565, upload-time = "2025-06-14T20:53:35.262Z" }, + { url = "https://files.pythonhosted.org/packages/9b/37/ee6e0bdd31b3b277fec00e5ee84d30eb1b5b8b0e025095e24ddc561697d0/pyobjc_framework_quartz-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9ac806067541917d6119b98d90390a6944e7d9bd737f5c0a79884202327c9204", size = 216410, upload-time = "2025-06-14T20:53:36.346Z" }, ] [[package]] name = "pyobjc-framework-quicklookthumbnailing" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/a1/35ca40d2d4ab05acbc9766986d482482d466529003711c7b4e52a8df4935/pyobjc_framework_quicklookthumbnailing-11.0.tar.gz", hash = "sha256:40763284bd0f71e6a55803f5234ad9cd8e8dd3aaaf5e1fd204e6c952b3f3530d", size = 16784, upload-time = "2025-01-14T19:05:09.857Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/98/6e87f360c2dfc870ae7870b8a25fdea8ddf1d62092c755686cebe7ec1a07/pyobjc_framework_quicklookthumbnailing-11.1.tar.gz", hash = "sha256:1614dc108c1d45bbf899ea84b8691288a5b1d25f2d6f0c57dfffa962b7a478c3", size = 16527, upload-time = "2025-06-14T20:58:20.811Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/85/1a66fefa99e7a4eb7534b2f56f9a24d33beda450dd2ca45d180307e76c74/pyobjc_framework_QuickLookThumbnailing-11.0-py2.py3-none-any.whl", hash = "sha256:6e567a764942845ce4db7ccfc0f8a9d091216bd029ecca955e618a43d64a5d84", size = 4164, upload-time = "2025-01-14T18:58:16.381Z" }, - { url = "https://files.pythonhosted.org/packages/05/d7/26decb13136b7c95a1ca3ecf202644ad2fd515a57e1117c71bfc86429b20/pyobjc_framework_QuickLookThumbnailing-11.0-py3-none-any.whl", hash = "sha256:e0f7f62b9a1df55e5f717518baf3260dc2cb8a9722cc5e9c6fffc643f69bda27", size = 4229, upload-time = "2025-01-14T18:58:17.404Z" }, + { url = "https://files.pythonhosted.org/packages/65/4a/ddc35bdcd44278f22df2154a52025915dba6c80d94e458d92e9e7430d1e4/pyobjc_framework_quicklookthumbnailing-11.1-py2.py3-none-any.whl", hash = "sha256:4d1863c6c83c2a199c1dbe704b4f8b71287168f4090ed218d37dc59277f0d9c9", size = 4219, upload-time = "2025-06-14T20:53:43.198Z" }, ] [[package]] name = "pyobjc-framework-replaykit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/43/c751c517dbb8ee599a31e59832c01080473c7964b6996ca29906f46c0967/pyobjc_framework_replaykit-11.0.tar.gz", hash = "sha256:e5693589423eb9ad99d63a7395169f97b484a58108321877b0fc27c748344593", size = 25589, upload-time = "2025-01-14T19:05:10.791Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/4f/014e95f0fd6842d7fcc3d443feb6ee65ac69d06c66ffa9327fc33ceb7c27/pyobjc_framework_replaykit-11.1.tar.gz", hash = "sha256:6919baa123a6d8aad769769fcff87369e13ee7bae11b955a8185a406a651061b", size = 26132, upload-time = "2025-06-14T20:58:21.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/56/89a8544426a46bf176c9462511c08d4c94ae7e0403abb2d73632af68ee8e/pyobjc_framework_ReplayKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:262fb834400e8379f4c795e65137763348992f3010284602d876050b8adb9ea4", size = 9904, upload-time = "2025-01-14T18:58:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/47/af/9abfa41060efc96000cc9ae77f302bb8210f3be0f793ba5d11f98a03e468/pyobjc_framework_ReplayKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:da9db123ee52761a670c6e41e5f9d9a47a2ca5582a9c4a7c8662a8bb56a0f593", size = 9903, upload-time = "2025-01-14T18:58:20.222Z" }, + { url = "https://files.pythonhosted.org/packages/72/97/2b4fbd52c6727977c0fdbde2b4a15226a9beb836248c289781e4129394e4/pyobjc_framework_replaykit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d88c3867349865d8a3a06ea064f15aed7e5be20d22882ac8a647d9b6959594e", size = 10066, upload-time = "2025-06-14T20:53:45.555Z" }, + { url = "https://files.pythonhosted.org/packages/b9/73/846cebb36fc279df18f10dc3a27cba8fe2e47e95350a3651147e4d454719/pyobjc_framework_replaykit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:22c6d09be9a6e758426d723a6c3658ad6bbb66f97ba9a1909bfcf29a91d99921", size = 10087, upload-time = "2025-06-14T20:53:46.242Z" }, ] [[package]] name = "pyobjc-framework-safariservices" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/ec/c9a97b1aa713145cc8c522c4146af06b293cfe1a959a03ee91007949533b/pyobjc_framework_safariservices-11.0.tar.gz", hash = "sha256:dba416bd0ed5f4481bc400bf56ce57e982c19feaae94bc4eb75d8bda9af15b7e", size = 34367, upload-time = "2025-01-14T19:05:12.914Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/fc/c47d2abf3c1de6db21d685cace76a0931d594aa369e3d090260295273f6e/pyobjc_framework_safariservices-11.1.tar.gz", hash = "sha256:39a17df1a8e1c339457f3acbff0dc0eae4681d158f9d783a11995cf484aa9cd0", size = 34905, upload-time = "2025-06-14T20:58:22.492Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/39/d69f8e7dbf6f366cb5fdaa8aa7ceef1dadb93a5e4d9fc63217477bba5e32/pyobjc_framework_SafariServices-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:55c02a533073e0a2aaf6db544f087fd861bace6b62035c3bb2e6b20f0b921b2b", size = 7262, upload-time = "2025-01-14T18:58:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/a625330bdf7a5d9962299562b6e19f6cbd1ea1b14887958e42a4372d3344/pyobjc_framework_SafariServices-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31ba086a39ee06d8622a504e3ea3a1f6dc8fab1d4c4c7930d5af6e989f38ec56", size = 7262, upload-time = "2025-01-14T18:58:30.725Z" }, + { url = "https://files.pythonhosted.org/packages/c9/aa/0c9f3456a57dbee711210a0ac3fe58aff9bf881ab7c65727b885193eb8af/pyobjc_framework_safariservices-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a441a2e99f7d6475bea00c3d53de924143b8f90052be226aee16f1f6d9cfdc8c", size = 7262, upload-time = "2025-06-14T20:53:52.057Z" }, + { url = "https://files.pythonhosted.org/packages/d7/13/9636e9d3dc362daaaa025b2aa4e28606a1e197dfc6506d3a246be8315f8a/pyobjc_framework_safariservices-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c92eb9e35f98368ea1bfaa8cdd41138ca8b004ea5a85833390a44e5626ca5061", size = 7275, upload-time = "2025-06-14T20:53:53.075Z" }, ] [[package]] name = "pyobjc-framework-safetykit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/30/89bfdbdca93e57b19891ddeff1742b20a2019cdeb2e44902027dce2642e1/pyobjc_framework_safetykit-11.0.tar.gz", hash = "sha256:9ec996a6a8eecada4b9fd1138244bcffea96a37722531f0ec16566049dfd4cdb", size = 20745, upload-time = "2025-01-14T19:05:13.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/cc/f6aa5d6f45179bd084416511be4e5b0dd0752cb76daa93869e6edb806096/pyobjc_framework_safetykit-11.1.tar.gz", hash = "sha256:c6b44e0cf69e27584ac3ef3d8b771d19a7c2ccd9c6de4138d091358e036322d4", size = 21240, upload-time = "2025-06-14T20:58:23.132Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/c5/68b79c0f128eb735397aa68a40e5ac48b88c12967f69358f25f753a3fc1c/pyobjc_framework_SafetyKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:83a1f313c9c63ba107a7c543a8300ae225fa5ff17d963b1c499859da45ceaf55", size = 8395, upload-time = "2025-01-14T18:58:35.46Z" }, - { url = "https://files.pythonhosted.org/packages/99/02/2853a00e75cca8db8b5053ff2648ff2a26f5c02f07af1c70630a36b58d04/pyobjc_framework_SafetyKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c6dd23fcaca9c41d6aadf2ca0a6d07c4032a0c4ea8873ee06da6efd1e868f97e", size = 8418, upload-time = "2025-01-14T18:58:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/1e9c661510cc4cd96f2beffc7ba39af36064c742e265303c689e85aaa0ad/pyobjc_framework_safetykit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3333e8e53a1e8c8133936684813a2254e5d1b4fe313333a3d0273e31b9158cf7", size = 8513, upload-time = "2025-06-14T20:53:58.413Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8f/6f4c833e31526a81faef9bf19695b332ba8d2fa53d92640abd6fb3ac1d78/pyobjc_framework_safetykit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b76fccdb970d3d751a540c47712e9110afac9abea952cb9b7bc0d5867db896e3", size = 8523, upload-time = "2025-06-14T20:53:59.443Z" }, ] [[package]] name = "pyobjc-framework-scenekit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/3f/a2761585399e752bce8275c9d56990d4b83e57b13d06dd98335891176a89/pyobjc_framework_scenekit-11.0.tar.gz", hash = "sha256:c0f37019f8de2a583f66e6d14dfd4ae23c8d8703e93f61c1c91728a21f62cd26", size = 213647, upload-time = "2025-01-14T19:05:15.129Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/cf/2d89777120d2812e7ee53c703bf6fc8968606c29ddc1351bc63f0a2a5692/pyobjc_framework_scenekit-11.1.tar.gz", hash = "sha256:82941f1e5040114d6e2c9fd35507244e102ef561c637686091b71a7ad0f31306", size = 214118, upload-time = "2025-06-14T20:58:24.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/4c/5ec624ae043fbbe15be2a989e3fc6cb08d992e0a5061450b84b33f96429c/pyobjc_framework_SceneKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86d23456e4c7a7bb7bb49be2b98647678ac7a39955e6bb242e0ac125d8b770e8", size = 33108, upload-time = "2025-01-14T18:58:43.577Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7f/fef1cf3eaf1366a6f3f93c5a6b164acfdfdc2d15b3243b70763ac217ce03/pyobjc_framework_SceneKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d0a0d557167adddf27a42fb109a1dce29a22ff09aca34558fccd1c22f08ae2b4", size = 33130, upload-time = "2025-01-14T18:58:44.549Z" }, + { url = "https://files.pythonhosted.org/packages/51/46/d011b5a88e45d78265f5df144759ff57e50d361d44c9adb68c2fb58b276d/pyobjc_framework_scenekit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3e777dacb563946ad0c2351e6cfe3f16b8587a65772ec0654e2be9f75764d234", size = 33490, upload-time = "2025-06-14T20:54:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f9/bdcd8a4bc6c387ef07f3e2190cea6a03d4f7ed761784f492b01323e8d900/pyobjc_framework_scenekit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c803d95b30c4ce49f46ff7174806f5eb84e4c3a152f8f580c5da0313c5c67041", size = 33558, upload-time = "2025-06-14T20:54:05.59Z" }, ] [[package]] name = "pyobjc-framework-screencapturekit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-coremedia" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/90/71f10db2f52ea324f82eaccc959442c43d21778cc5b1294c29e1942e635c/pyobjc_framework_screencapturekit-11.0.tar.gz", hash = "sha256:ca2c960e28216e56f33e4ca9b9b1eda12d9c17b719bae727181e8b96f0314c4b", size = 53046, upload-time = "2025-01-14T19:05:16.834Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/a5/9bd1f1ad1773a1304ccde934ff39e0f0a0b0034441bf89166aea649606de/pyobjc_framework_screencapturekit-11.1.tar.gz", hash = "sha256:11443781a30ed446f2d892c9e6642ca4897eb45f1a1411136ca584997fa739e0", size = 53548, upload-time = "2025-06-14T20:58:24.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/aa/d6d0818564570065411874cbe3de86dee105dc9906161c0584009a1a63bc/pyobjc_framework_ScreenCaptureKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:38468e833ec1498778bd33ce30578afed2e13ac14c73e8e6290ff06a2e0c50d8", size = 11110, upload-time = "2025-01-14T18:58:51.475Z" }, - { url = "https://files.pythonhosted.org/packages/27/61/557e725aef9ad76a1a7c48b361f8c5636a606cbaf9ba520ff8f69d3cf791/pyobjc_framework_ScreenCaptureKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7d8a83dcc0950699242677cfefda545b9c0a0567111f8f3d3df1cf6ed75ea480", size = 11121, upload-time = "2025-01-14T18:58:53.055Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e0/fd1957e962c4a1624171dbbda4e425615848a7bcc9b45a524018dc449874/pyobjc_framework_screencapturekit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7203108d28d7373501c455cd4a8bbcd2eb7849906dbc7859ac17a350b141553c", size = 11280, upload-time = "2025-06-14T20:54:11.699Z" }, + { url = "https://files.pythonhosted.org/packages/98/37/840f306dcf01dd2bd092ae8dcf371a3bad3a0f88f0780d0840f899a8c047/pyobjc_framework_screencapturekit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:641fa7834f54558859209e174c83551d5fa239ca6943ace52665f7d45e562ff2", size = 11308, upload-time = "2025-06-14T20:54:12.382Z" }, ] [[package]] name = "pyobjc-framework-screensaver" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/b6/71c20259a1bfffcb5103be62564006b1bbc21f80180658101e2370683bcb/pyobjc_framework_screensaver-11.0.tar.gz", hash = "sha256:2e4c643624cc0cffeafc535c43faf5f8de8be030307fa8a5bea257845e8af474", size = 23774, upload-time = "2025-01-14T19:05:19.325Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/f6/f2d48583b29fc67b64aa1f415fd51faf003d045cdb1f3acab039b9a3f59f/pyobjc_framework_screensaver-11.1.tar.gz", hash = "sha256:d5fbc9dc076cc574ead183d521840b56be0c160415e43cb8e01cfddd6d6372c2", size = 24302, upload-time = "2025-06-14T20:58:25.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/ab/f17cd36458e6cf6d64c412128641edcfc220b8147283f6b34ef56c7db111/pyobjc_framework_ScreenSaver-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:436357c822d87220df64912da04b421e82a5e1e6464d48f2dbccc69529d19cd3", size = 8445, upload-time = "2025-01-14T18:58:59.299Z" }, - { url = "https://files.pythonhosted.org/packages/52/57/300b641e929741a5d38cf80c74496918be1d2fe5e210d3fceb3e768747b2/pyobjc_framework_ScreenSaver-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:03b12e89bc164cb01527ca795f3f590f286d15de6ee0e4ff1d36705740d6d72f", size = 8372, upload-time = "2025-01-14T18:59:00.358Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8c/2236e5796f329a92ce7664036da91e91d63d86217972dc2939261ce88dde/pyobjc_framework_screensaver-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8b959761fddf06d9fb3fed6cd0cea6009d60473317e11490f66dcf0444011d5f", size = 8466, upload-time = "2025-06-14T20:54:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/f9/4ae982c7a1387b64954130b72187e140329b73c647acb4d6b6eb3c033d8d/pyobjc_framework_screensaver-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f2d22293cf9d715e4692267a1678096afd6793c0519d9417cf77c8a6c706a543", size = 8402, upload-time = "2025-06-14T20:54:19.044Z" }, ] [[package]] name = "pyobjc-framework-screentime" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/a7/ee60ee5b0471a4367eaa1c8a243418874fd48fac5dbdfdd318a653d94aaa/pyobjc_framework_screentime-11.0.tar.gz", hash = "sha256:6dd74dc64be1865346fcff63b8849253697f7ac68d83ee2708019cf3852c1cd7", size = 14398, upload-time = "2025-01-14T19:05:21.547Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/33/ebed70a1de134de936bb9a12d5c76f24e1e335ff4964f9bb0af9b09607f1/pyobjc_framework_screentime-11.1.tar.gz", hash = "sha256:9bb8269456bbb674e1421182efe49f9168ceefd4e7c497047c7bf63e2f510a34", size = 14875, upload-time = "2025-06-14T20:58:26.179Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/7a/8df61f80725e993fd0dc1a111217de6a8efec35b02a4796749de0b7e8c34/pyobjc_framework_ScreenTime-11.0-py2.py3-none-any.whl", hash = "sha256:723938c7d47e3c5c1c0f79010a01139762384bd0c03c51ee7a4736fc3f128fed", size = 3721, upload-time = "2025-01-14T18:59:04.027Z" }, - { url = "https://files.pythonhosted.org/packages/c4/62/2f86cedd4cc439625976848832c1d1571fcb69cc087dd71c9cf09e793db5/pyobjc_framework_ScreenTime-11.0-py3-none-any.whl", hash = "sha256:45db846ec9249cab90e86cbb31cf70e13800305b7c74819ab681a91854c91df2", size = 3790, upload-time = "2025-01-14T18:59:06.363Z" }, + { url = "https://files.pythonhosted.org/packages/ea/20/783eccea7206ceeda42a09a4614e3da92889e4c54abe9dec2e5e53576e1a/pyobjc_framework_screentime-11.1-py2.py3-none-any.whl", hash = "sha256:50a4e4ab33d6643a52616e990aa1c697d5e3e8f9f9bdab8d631e6d42d8287b4f", size = 3949, upload-time = "2025-06-14T20:54:26.916Z" }, ] [[package]] name = "pyobjc-framework-scriptingbridge" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/f0/592af19047935e44c07ddd1eba4f05aa8eb460ee842f7d5d48501231cd69/pyobjc_framework_scriptingbridge-11.0.tar.gz", hash = "sha256:65e5edd0ea608ae7f01808b963dfa25743315f563705d75c493c2fa7032f88cc", size = 22626, upload-time = "2025-01-14T19:05:22.461Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/c1/5b1dd01ff173df4c6676f97405113458918819cb2064c1735b61948e8800/pyobjc_framework_scriptingbridge-11.1.tar.gz", hash = "sha256:604445c759210a35d86d3e0dfcde0aac8e5e3e9d9e35759e0723952138843699", size = 23155, upload-time = "2025-06-14T20:58:26.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/2c/2fd33c0318a8fe35f00f0089a44a2c27d4d0fd0b4b5e13628051a4d8c9d3/pyobjc_framework_ScriptingBridge-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c98d080446aa8ba4074e43eb0be1feed96781dbc0718496f172fcd20e84a9158", size = 8209, upload-time = "2025-01-14T18:59:11.107Z" }, - { url = "https://files.pythonhosted.org/packages/93/3b/b2b721248e951eef6b7e6b25cb3a1d6683702235bc73683d0239f068d2df/pyobjc_framework_ScriptingBridge-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:23a4b2e2e57b7b4d992777ea9efb15273ccd8e8105385143dab9bd5a10962317", size = 8238, upload-time = "2025-01-14T18:59:13.27Z" }, + { url = "https://files.pythonhosted.org/packages/e6/76/e173ca0b121693bdc6ac5797b30fd5771f31a682d15fd46402dc6f9ca3d1/pyobjc_framework_scriptingbridge-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6020c69c14872105852ff99aab7cd2b2671e61ded3faefb071dc40a8916c527", size = 8301, upload-time = "2025-06-14T20:54:29.082Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/31849063e3e81b4c312ce838dc98f0409c09eb33bc79dbb5261cb994a4c4/pyobjc_framework_scriptingbridge-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:226ba12d9cbd504411b702323b0507dd1690e81b4ce657c5f0d8b998c46cf374", size = 8323, upload-time = "2025-06-14T20:54:30.105Z" }, ] [[package]] name = "pyobjc-framework-searchkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-coreservices" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/27/9676327cf7d13346d546325b411a5deaa072bd0fbe733c8aae8a9a00c0e0/pyobjc_framework_searchkit-11.0.tar.gz", hash = "sha256:36f3109e74bc5e6fab60c02be804d5ed1c511ad51ea0d597a6c6a9653573ddf5", size = 31182, upload-time = "2025-01-14T19:05:24.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/20/61b73fddae0d1a94f5defb0cd4b4f391ec03bfcce7ebe830cb827d5e208a/pyobjc_framework_searchkit-11.1.tar.gz", hash = "sha256:13a194eefcf1359ce9972cd92f2aadddf103f3efb1b18fd578ba5367dff3c10c", size = 30918, upload-time = "2025-06-14T20:58:27.447Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/d4/64fa608b5d91859b11c26ceca83a41d2bf1d0dcbf1d9df847bab5a52ccc8/pyobjc_framework_SearchKit-11.0-py2.py3-none-any.whl", hash = "sha256:332f9d30ec3b223efaac681fbdd923ba660575e241abb4ed5e03207c97799530", size = 3633, upload-time = "2025-01-14T18:59:18.343Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/83e94c505c5436821982d724cc890f74d717f9473782f7278ce78634685d/pyobjc_framework_SearchKit-11.0-py3-none-any.whl", hash = "sha256:5f4304cb77c327b28ac0f7ec9b99313075afd742091d39368eb64f076bb7d141", size = 3699, upload-time = "2025-01-14T18:59:20.754Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ed/a118d275a9132c8f5adcd353e4d9e844777068e33d51b195f46671161a7f/pyobjc_framework_searchkit-11.1-py2.py3-none-any.whl", hash = "sha256:9c9d6ca71cef637ccc3627225fb924a460b3d0618ed79bb0b3c12fcbe9270323", size = 3714, upload-time = "2025-06-14T20:54:34.329Z" }, ] [[package]] name = "pyobjc-framework-security" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/75/4b916bff8c650e387077a35916b7a7d331d5ff03bed7275099d96dcc6cd9/pyobjc_framework_security-11.0.tar.gz", hash = "sha256:ac078bb9cc6762d6f0f25f68325dcd7fe77acdd8c364bf4378868493f06a0758", size = 347059, upload-time = "2025-01-14T19:05:26.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/6f/ba50ed2d9c1192c67590a7cfefa44fc5f85c776d1e25beb224dec32081f6/pyobjc_framework_security-11.1.tar.gz", hash = "sha256:dabcee6987c6bae575e2d1ef0fcbe437678c4f49f1c25a4b131a5e960f31a2da", size = 302291, upload-time = "2025-06-14T20:58:28.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/d8/092940f8c46cf09000a9d026e9854772846d5335e3e8a44d0a81aa1f359e/pyobjc_framework_Security-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:93bc23630563de2551ac49048af010ac9cb40f927cc25c898b7cc48550ccd526", size = 41499, upload-time = "2025-01-14T18:59:22.819Z" }, - { url = "https://files.pythonhosted.org/packages/0b/fc/8710bbe80b825c97ecc312aaead3b0f606a23b62b895f6e0a07df8bfeeae/pyobjc_framework_Security-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:421e03b8560ed296a7f5ee67f42f5f978f8c7959d65c8fec99cd77dc65786355", size = 41523, upload-time = "2025-01-14T18:59:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ae/1679770d9a1cf5f2fe532a3567a51f0c5ee09054ae2c4003ae8f3e11eea4/pyobjc_framework_security-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d361231697486e97cfdafadf56709190696ab26a6a086dbba5f170e042e13daa", size = 41202, upload-time = "2025-06-14T20:54:36.255Z" }, + { url = "https://files.pythonhosted.org/packages/35/16/7fc52ab1364ada5885bf9b4c9ea9da3ad892b847c9b86aa59e086b16fc11/pyobjc_framework_security-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2eb4ba6d8b221b9ad5d010e026247e8aa26ee43dcaf327e848340ed227d22d7e", size = 41222, upload-time = "2025-06-14T20:54:37.032Z" }, ] [[package]] name = "pyobjc-framework-securityfoundation" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-security" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/d6/0d817edb11d2bdb0f536059e913191e587f1984e39397bb3341209d92c21/pyobjc_framework_securityfoundation-11.0.tar.gz", hash = "sha256:5ae906ded5dd40046c013a7e0c1f59416abafb4b72bc947b6cd259749745e637", size = 13526, upload-time = "2025-01-14T19:05:27.275Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/d4/19591dd0938a45b6d8711ef9ae5375b87c37a55b45d79c52d6f83a8d991f/pyobjc_framework_securityfoundation-11.1.tar.gz", hash = "sha256:b3c4cf70735a93e9df40f3a14478143959c415778f27be8c0dc9ae0c5b696b92", size = 13270, upload-time = "2025-06-14T20:58:29.304Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/41/50da30e87841c8b9ee1f17e9720dc9dbb2c2e59abac84fffe899ed5f9188/pyobjc_framework_SecurityFoundation-11.0-py2.py3-none-any.whl", hash = "sha256:8f8e43b91ae7cb45f3251c14c0c6caf5fdcdb93794176c4b118214a108ee2ef3", size = 3716, upload-time = "2025-01-14T18:59:29.79Z" }, - { url = "https://files.pythonhosted.org/packages/cb/61/e73a61de62e31b33378ee635534228f4801b1554fbd89a47e0b36965908d/pyobjc_framework_SecurityFoundation-11.0-py3-none-any.whl", hash = "sha256:1fa89969fbf7a4fd57214388a43f7ed6b6b1fd0c0ec7aa77752444eb1604143c", size = 3787, upload-time = "2025-01-14T18:59:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ab/23db6b1c09810d6bcc4eab96e62487fb4284b57e447eabe6c001cb41e36d/pyobjc_framework_securityfoundation-11.1-py2.py3-none-any.whl", hash = "sha256:25f2cf10f80c122f462e9d4d43efe9fd697299c194e0c357e76650e234e6d286", size = 3772, upload-time = "2025-06-14T20:54:41.732Z" }, ] [[package]] name = "pyobjc-framework-securityinterface" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-security" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/88/d7c4942650707fe5b1d3b45b42684f58f2cab7d2772ec74ca96ecef575eb/pyobjc_framework_securityinterface-11.0.tar.gz", hash = "sha256:8843a27cf30a8e4dd6e2cb7702a6d65ad4222429f0ccc6c062537af4683b1c08", size = 37118, upload-time = "2025-01-14T19:05:28.569Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/be/c846651c3e7f38a637c40ae1bcda9f14237c2395637c3a188df4f733c727/pyobjc_framework_securityinterface-11.1.tar.gz", hash = "sha256:e7aa6373e525f3ae05d71276e821a6348c53fec9f812b90eec1dbadfcb507bc9", size = 37648, upload-time = "2025-06-14T20:58:29.932Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/a96da5f43da5a9d0e5d016bc672a4dca09f88d091c96d9ecff5f753ad1d5/pyobjc_framework_SecurityInterface-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2771dae043c8aa278887f96c7d206957164c7a81a562fa391bf0b9316d6755eb", size = 10706, upload-time = "2025-01-14T18:59:32.632Z" }, - { url = "https://files.pythonhosted.org/packages/50/86/fc41dcf8f5300ad2c6508568535d9c0a83b412b0a4a961616441c8acf10f/pyobjc_framework_SecurityInterface-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6453732f7608d514e8f7005d80d238422cbebc4ab4d6d6fed1e51175f9f7244f", size = 10781, upload-time = "2025-01-14T18:59:33.832Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ec/8073f37f56870efb039970f1cc4536f279c5d476abab2e8654129789277f/pyobjc_framework_securityinterface-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3e884620b22918d462764f0665f6ac0cbb8142bb160fcd27c4f4357f81da73b7", size = 10769, upload-time = "2025-06-14T20:54:43.344Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/48b8027a24f3f8924f5be5f97217961b4ed23e6be49b3bd94ee8a0d56a1e/pyobjc_framework_securityinterface-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:26056441b325029da06a7c7b8dd1a0c9a4ad7d980596c1b04d132a502b4cacc0", size = 10837, upload-time = "2025-06-14T20:54:44.052Z" }, +] + +[[package]] +name = "pyobjc-framework-securityui" +version = "11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-security" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/5b/3b5585d56e0bcaba82e0661224bbc7aaf29fba6b10498971dbe08b2b490a/pyobjc_framework_securityui-11.1.tar.gz", hash = "sha256:e80c93e8a56bf89e4c0333047b9f8219752dd6de290681e9e2e2b2e26d69e92d", size = 12179, upload-time = "2025-06-14T20:58:30.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/a4/c9fcc42065b6aed73b14b9650c1dc0a4af26a30d418cbc1bab33621b461c/pyobjc_framework_securityui-11.1-py2.py3-none-any.whl", hash = "sha256:3cdb101b03459fcf8e4064b90021d06761003f669181e02f43ff585e6ba2403d", size = 3581, upload-time = "2025-06-14T20:54:49.474Z" }, ] [[package]] name = "pyobjc-framework-sensitivecontentanalysis" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/e4/f1e0f150ae6c6ad7dde9b248f34f324f4f8b1c42260dbf62420f80d79ba9/pyobjc_framework_sensitivecontentanalysis-11.0.tar.gz", hash = "sha256:0f09034688f894c0f4409c16adaf857d78714d55472de4aa2ac40fbd7ba233d6", size = 13060, upload-time = "2025-01-14T19:05:29.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/7b/e28f6b30d99e9d464427a07ada82b33cd3292f310bf478a1824051d066b9/pyobjc_framework_sensitivecontentanalysis-11.1.tar.gz", hash = "sha256:5b310515c7386f7afaf13e4632d7d9590688182bb7b563f8026c304bdf317308", size = 12796, upload-time = "2025-06-14T20:58:31.488Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/eb/e0d60b3e233860a237fdddd44ab961c9115c33e947058d73c222dafc50af/pyobjc_framework_SensitiveContentAnalysis-11.0-py2.py3-none-any.whl", hash = "sha256:e19d2edc807f98aef31fa4db5472a509cf90523436c971d1095a000b0e357058", size = 3791, upload-time = "2025-01-14T18:59:39.563Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1c/fb2138cf08cd0215ea4f78032871a1d89e7e41d9fad18b55e937f0577c03/pyobjc_framework_SensitiveContentAnalysis-11.0-py3-none-any.whl", hash = "sha256:027bd0be0785f7aea3bfd56ff7c3496e5d383211122393c599c28ea392675589", size = 3863, upload-time = "2025-01-14T18:59:40.548Z" }, + { url = "https://files.pythonhosted.org/packages/3c/63/76a939ecac74ca079702165330c692ad2c05ff9b2b446a72ddc8cdc63bb9/pyobjc_framework_sensitivecontentanalysis-11.1-py2.py3-none-any.whl", hash = "sha256:dbb78f5917f986a63878bb91263bceba28bd86fc381bad9461cf391646db369f", size = 3852, upload-time = "2025-06-14T20:54:50.75Z" }, ] [[package]] name = "pyobjc-framework-servicemanagement" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/59/8d38b5cdbcfb57ab842e080436dbd04d5a5d2080e99a2ea1e286cfad12a8/pyobjc_framework_servicemanagement-11.0.tar.gz", hash = "sha256:10b1bbcee3de5bb2b9fc3d6763eb682b7a1d9ddd4bd2c882fece62783cb17885", size = 16882, upload-time = "2025-01-14T19:05:30.537Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c6/32e11599d9d232311607b79eb2d1d21c52eaaf001599ea85f8771a933fa2/pyobjc_framework_servicemanagement-11.1.tar.gz", hash = "sha256:90a07164da49338480e0e135b445acc6ae7c08549a2037d1e512d2605fedd80a", size = 16645, upload-time = "2025-06-14T20:58:32.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/35/cbac7db272d0e5e71b300be1517b0a1dc7cf035944675eaed7066d41e883/pyobjc_framework_ServiceManagement-11.0-py2.py3-none-any.whl", hash = "sha256:35cfd7a369a120fa55e64b719a2dda00295b2cc6ddab16ffa8939f4326d1b37d", size = 5254, upload-time = "2025-01-14T18:59:41.438Z" }, - { url = "https://files.pythonhosted.org/packages/b3/40/26c5d63d131e3e415815bfbb4bd035ba10d45f0d87733646221966871b6b/pyobjc_framework_ServiceManagement-11.0-py3-none-any.whl", hash = "sha256:7ec19c9632f67d589ad37815d001e8e443d92e75001c370486a1070a4359e166", size = 5322, upload-time = "2025-01-14T18:59:42.585Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f1/222462f5afcb6cb3c1fc9e6092dfcffcc7eb9db8bd2cef8c1743a22fbe95/pyobjc_framework_servicemanagement-11.1-py2.py3-none-any.whl", hash = "sha256:104f56557342a05ad68cd0c9daf63b7f4678957fe1f919f03a872f1607a50710", size = 5338, upload-time = "2025-06-14T20:54:51.614Z" }, ] [[package]] name = "pyobjc-framework-sharedwithyou" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-sharedwithyoucore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/84/db667061f815537717a6cac891df01a45b65e6feaa2dfa0c9d2e3803a1ef/pyobjc_framework_sharedwithyou-11.0.tar.gz", hash = "sha256:a3a03daac77ad7364ed22109ca90c6cd2dcb7611a96cbdf37d30543ef1579399", size = 33696, upload-time = "2025-01-14T19:05:31.396Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/a5/e299fbd0c13d4fac9356459f21372f6eef4279d0fbc99ba316d88dfbbfb4/pyobjc_framework_sharedwithyou-11.1.tar.gz", hash = "sha256:ece3a28a3083d0bcad0ac95b01f0eb699b9d2d0c02c61305bfd402678753ff6e", size = 34216, upload-time = "2025-06-14T20:58:32.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/ab/391ef0de3021997ec9a12d8044c0b7e884780a9bead7f847254e06d0f075/pyobjc_framework_SharedWithYou-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6dac74375d3dc18d67cae46f3f16a45cef699b1976a4012827c0f15256da55df", size = 8606, upload-time = "2025-01-14T18:59:44.537Z" }, - { url = "https://files.pythonhosted.org/packages/cf/04/6a3eb12bf9c35f3063be678f36430beb92b7e2683f4b952596396473a74d/pyobjc_framework_SharedWithYou-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6076a0893a3597e054918c136f3391671a225a37fe1b1a070046817e3a232954", size = 8629, upload-time = "2025-01-14T18:59:45.579Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/7caefaddc58702da830d1cc4eb3c45ae82dcd605ea362126ab47ebd54f7d/pyobjc_framework_sharedwithyou-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ce1c37d5f8cf5b0fe8a261e4e7256da677162fd5aa7b724e83532cdfe58d8f94", size = 8725, upload-time = "2025-06-14T20:54:53.179Z" }, + { url = "https://files.pythonhosted.org/packages/57/44/211e1f18676e85d3656671fc0c954ced2cd007e55f1b0b6b2e4d0a0852eb/pyobjc_framework_sharedwithyou-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:99e1749187ae370be7b9c55dd076d1b8143f0d8db3e83f52540586f32e7abb33", size = 8740, upload-time = "2025-06-14T20:54:53.879Z" }, ] [[package]] name = "pyobjc-framework-sharedwithyoucore" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/2a/86904cd9cc3bf5cdb9101481e17e67358f39f81ffa0f36768097287e34b3/pyobjc_framework_sharedwithyoucore-11.0.tar.gz", hash = "sha256:3932452677df5d67ea27845ab26ccaaa1d1779196bf16b62c5655f13d822c82d", size = 28877, upload-time = "2025-01-14T19:05:32.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/a3/1ca6ff1b785772c7c5a38a7c017c6f971b1eda638d6a0aab3bbde18ac086/pyobjc_framework_sharedwithyoucore-11.1.tar.gz", hash = "sha256:790050d25f47bda662a9f008b17ca640ac2460f2559a56b17995e53f2f44ed73", size = 29459, upload-time = "2025-06-14T20:58:33.422Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/40/69ae712e223991cd975c1f8ba2b00a5aa4c129ac0e76838b4d936740e4c7/pyobjc_framework_SharedWithYouCore-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:46cd00a97c5fec747ef057000daa88495699ea5d5d6fe1f302bfb89b2d431645", size = 8366, upload-time = "2025-01-14T18:59:55.641Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ce/500ad643f2d07e8ef065e8ddc5a08954f5d59cc199c89b700581eaf821ee/pyobjc_framework_SharedWithYouCore-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8b5f180371a63da718fe6c3b58e7613c6b2adf9b483cefbf6d9467eb8ac2f0ca", size = 8380, upload-time = "2025-01-14T18:59:56.546Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/08cfa01dcdb4655514b7a10eb7c40da2bdb7866078c761d6ed26c9f464f7/pyobjc_framework_sharedwithyoucore-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a7fe5ffcc65093ef7cd25903769ad557c3d3c5a59155a31f3f934cf555101e6", size = 8489, upload-time = "2025-06-14T20:54:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/3b2e13fcf393aa434b1cf5c29c6aaf65ee5b8361254df3a920ed436bb5e4/pyobjc_framework_sharedwithyoucore-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dd18c588b29de322c25821934d6aa6d2bbbdbb89b6a4efacdb248b4115fc488d", size = 8512, upload-time = "2025-06-14T20:55:00.411Z" }, ] [[package]] name = "pyobjc-framework-shazamkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/2a/1f4ad92260860e500cb61119e8e7fe604b0788c32f5b00446b5a56705a2b/pyobjc_framework_shazamkit-11.0.tar.gz", hash = "sha256:cea736cefe90b6bb989d0a8abdc21ef4b3b431b27657abb09d6deb0b2c1bd37a", size = 25172, upload-time = "2025-01-14T19:05:34.497Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/08/ba739b97f1e441653bae8da5dd1e441bbbfa43940018d21edb60da7dd163/pyobjc_framework_shazamkit-11.1.tar.gz", hash = "sha256:c6e3c9ab8744d9319a89b78ae6f185bb5704efb68509e66d77bcd1f84a9446d6", size = 25797, upload-time = "2025-06-14T20:58:34.086Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/81/edfcd4be626aae356dd1b991f521eaeffa1798e91ddae9e7d9ae8ed371d1/pyobjc_framework_ShazamKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ecdc2392d7e8d6e2540c7ad3073a229d08b0818c5dd044a26c93b765ce9868aa", size = 8411, upload-time = "2025-01-14T19:00:02.908Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/f3d2ae7a604e3e3c0de93ed229895be6757edfa0cc76f2a44670f28a81c8/pyobjc_framework_ShazamKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ef79d863cc7d4023aa552f55d4120653eceed862baf1edba8e08b1af10fab036", size = 8419, upload-time = "2025-01-14T19:00:05.081Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b6/c03bc9aad7f15979b5d7f144baf5161c3c40e0bca194cce82e1bce0804a9/pyobjc_framework_shazamkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2fe6990d0ec1b40d4efd0d0e49c2deb65198f49b963e6215c608c140b3149151", size = 8540, upload-time = "2025-06-14T20:55:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/89/b7/594b8bdc406603a7a07cdb33f2be483fed16aebc35aeb087385fc9eca844/pyobjc_framework_shazamkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b323f5409b01711aa2b6e2113306084fab2cc83fa57a0c3d55bd5876358b68d8", size = 8560, upload-time = "2025-06-14T20:55:07.564Z" }, ] [[package]] name = "pyobjc-framework-social" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/56/ed483f85105ef929241ab1a6ed3dbfd0be558bb900e36b274f997db9c869/pyobjc_framework_social-11.0.tar.gz", hash = "sha256:ccedd6eddb6744049467bce19b4ec4f0667ec60552731c02dcbfa8938a3ac798", size = 14806, upload-time = "2025-01-14T19:05:35.394Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/2e/cc7707b7a40df392c579087947049f3e1f0e00597e7151ec411f654d8bef/pyobjc_framework_social-11.1.tar.gz", hash = "sha256:fbc09d7b00dad45b547f9b2329f4dcee3f5a50e2348de1870de0bd7be853a5b7", size = 14540, upload-time = "2025-06-14T20:58:35.116Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/1d/2cc0f753ac8b1f5c15cfa9201d8584ff4de6dc940fc954cd9c52d1a615f9/pyobjc_framework_Social-11.0-py2.py3-none-any.whl", hash = "sha256:aa379009738afb0d6abc0347e8189f7f316109e9dfcb904f7f14e6b7c3d5bad8", size = 4362, upload-time = "2025-01-14T19:00:10.058Z" }, - { url = "https://files.pythonhosted.org/packages/a8/25/b762b1f9429f8ea0df754e7d58bafd48d73e5527b0423e67570661a7907e/pyobjc_framework_Social-11.0-py3-none-any.whl", hash = "sha256:94db183e8b3ad21272a1ba24e9cda763d603c6021fd80a96d00ce78b6b94e1c2", size = 4428, upload-time = "2025-01-14T19:00:11.242Z" }, + { url = "https://files.pythonhosted.org/packages/86/1d/e1026c082a66075dbb7e57983c0aaaed3ee09f06c346743e8af24d1dc21a/pyobjc_framework_social-11.1-py2.py3-none-any.whl", hash = "sha256:ab5878c47d7a0639704c191cee43eeb259e09688808f0905c42551b9f79e1d57", size = 4444, upload-time = "2025-06-14T20:55:12.536Z" }, ] [[package]] name = "pyobjc-framework-soundanalysis" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/14/697ca1b76228a96bb459f3cf43234798b05fdf11691202449d98d9d887af/pyobjc_framework_soundanalysis-11.0.tar.gz", hash = "sha256:f541fcd04ec5d7528dd2ae2d873a92a3092e87fb70b8df229c79defb4d807d1a", size = 16789, upload-time = "2025-01-14T19:05:36.576Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/d4/b9497dbb57afdf0d22f61bb6e776a6f46cf9294c890448acde5b46dd61f3/pyobjc_framework_soundanalysis-11.1.tar.gz", hash = "sha256:42cd25b7e0f343d8b59367f72b5dae96cf65696bdb8eeead8d7424ed37aa1434", size = 16539, upload-time = "2025-06-14T20:58:35.813Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/d4/91afb41c514d1e236567b971a981f96c1d20f16eb0658256369c53a4bf45/pyobjc_framework_SoundAnalysis-11.0-py2.py3-none-any.whl", hash = "sha256:5969096cadb07f9ba9855cedf6f53674ddb030a324b4981091834d1b31c8c27e", size = 4111, upload-time = "2025-01-14T19:00:13.327Z" }, - { url = "https://files.pythonhosted.org/packages/af/7a/f960ad1e727f6d917e6c84b7383f3eacbb2948bc60396be3bce40cbd8128/pyobjc_framework_SoundAnalysis-11.0-py3-none-any.whl", hash = "sha256:70f70923756e118203cde4ac25083a34ead69a6034baed9c694a36f5fe2325f3", size = 4182, upload-time = "2025-01-14T19:00:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/13/b4/7e8cf3a02e615239568fdf12497233bbd5b58082615cd28a0c7cd4636309/pyobjc_framework_soundanalysis-11.1-py2.py3-none-any.whl", hash = "sha256:6cf983c24fb2ad2aa5e7499ab2d30ff134d887fe91fd2641acf7472e546ab4e5", size = 4161, upload-time = "2025-06-14T20:55:13.342Z" }, ] [[package]] name = "pyobjc-framework-speech" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/39/e9f0a73243c38d85f8da6a1a2afda73503e2fcc31a72f5479770bceae0c1/pyobjc_framework_speech-11.0.tar.gz", hash = "sha256:92a191c3ecfe7032eea2140ab5dda826a59c7bb84b13a2edb0ebc471a76e6d7b", size = 40620, upload-time = "2025-01-14T19:05:38.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/76/2a1fd7637b2c662349ede09806e159306afeebfba18fb062ad053b41d811/pyobjc_framework_speech-11.1.tar.gz", hash = "sha256:d382977208c3710eacea89e05eae4578f1638bb5a7b667c06971e3d34e96845c", size = 41179, upload-time = "2025-06-14T20:58:36.43Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/85/e989076ff0cd40c7cfb3ed7d621703de11bfd8286f1729aca759db1f42a3/pyobjc_framework_Speech-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:353179210683e38bfbd675df6a35eec46b30ce30b7291bcb07a5cadaf11a3bd7", size = 9016, upload-time = "2025-01-14T19:00:17.661Z" }, - { url = "https://files.pythonhosted.org/packages/00/03/827acde068787c2318981e2bfef2c3cadbe8552434ccc0634b30084ef914/pyobjc_framework_Speech-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:134e08025f4638e428602f7e16bbec94b00477eec090316138d758a86e10fd5f", size = 9037, upload-time = "2025-01-14T19:00:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d3/c3b1d542c5ddc816924f02edf2ececcda226f35c91e95ed80f2632fbd91c/pyobjc_framework_speech-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3e0276a66d2fa4357959a6f6fb5def03f8e0fd3aa43711d6a81ab2573b9415f", size = 9171, upload-time = "2025-06-14T20:55:15.316Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/267f4699055beb39723ccbff70909ec3851e4adf17386f6ad85e5d983780/pyobjc_framework_speech-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7726eff52cfa9cc7178ddcd1285cbc23b5f89ee55b4b850b0d2e90bb4f8e044b", size = 9180, upload-time = "2025-06-14T20:55:16.556Z" }, ] [[package]] name = "pyobjc-framework-spritekit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/6e/642e64f5b62a7777c784931c7f018788b5620e307907d416c837fd0c4315/pyobjc_framework_spritekit-11.0.tar.gz", hash = "sha256:aa43927e325d4ac253b7c0ec4df95393b0354bd278ebe9871803419d12d1ef80", size = 129851, upload-time = "2025-01-14T19:05:39.709Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/02/2e253ba4f7fad6efe05fd5fcf44aede093f6c438d608d67c6c6623a1846d/pyobjc_framework_spritekit-11.1.tar.gz", hash = "sha256:914da6e846573cac8db5e403dec9a3e6f6edf5211f9b7e429734924d00f65108", size = 130297, upload-time = "2025-06-14T20:58:37.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/80/319f156ac6f6cab0dbc85881d81a74d4a7f17913256338683ae8d9ed56c4/pyobjc_framework_SpriteKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d0971a7a85786edc521ab897bdb0c78696278e6417bf389abdfe2151358e854", size = 18077, upload-time = "2025-01-14T19:00:26.815Z" }, - { url = "https://files.pythonhosted.org/packages/bb/09/303d76844a10745cdbac1ff76c2c8630c1ef46455014562dc79aaa72a6e3/pyobjc_framework_SpriteKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0da5f2b52636a2f04fc38a123fed9d7f8d6fd353df027c51c0bfc91e244a9d2b", size = 18145, upload-time = "2025-01-14T19:00:27.956Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/1c874cffba691cf8c103e0fdf55b53d9749577794efb9fc30e4394ffef41/pyobjc_framework_spritekit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1c8c94d37c054b6e3c22c237f6458c12649776e5ac921d066ab99dee2e580909", size = 17718, upload-time = "2025-06-14T20:55:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fe/39d92bf40ec7a6116f89fd95053321f7c00c50c10d82b9adfa0f9ebdb10c/pyobjc_framework_spritekit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8b470a890db69e70ef428dfff88da499500fca9b2d44da7120dc588d13a2dbdb", size = 17776, upload-time = "2025-06-14T20:55:23.639Z" }, ] [[package]] name = "pyobjc-framework-storekit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/ca/f4e5a1ff8c98bbbf208639b2bef7bf3b88936bccda1d8ed34aa7d052f589/pyobjc_framework_storekit-11.0.tar.gz", hash = "sha256:ef7e75b28f1fa8b0b6413e64b9d5d78b8ca358fc2477483d2783f688ff8d75e0", size = 75855, upload-time = "2025-01-14T19:05:41.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/a0/58cab9ebc9ac9282e1d4734b1987d1c3cd652b415ec3e678fcc5e735d279/pyobjc_framework_storekit-11.1.tar.gz", hash = "sha256:85acc30c0bfa120b37c3c5ac693fe9ad2c2e351ee7a1f9ea6f976b0c311ff164", size = 76421, upload-time = "2025-06-14T20:58:37.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/40/af53ad7781515866003c2c71056a053d2f033cf2aa31920a8a1fdb829d7a/pyobjc_framework_StoreKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1d51a05a5e0277c542978b1f5a6aa33331359de7c0a2cf0ad922760b36e5066a", size = 11655, upload-time = "2025-01-14T19:00:32.894Z" }, - { url = "https://files.pythonhosted.org/packages/f3/11/ba3259d3b22980e08c5e8255a48cc97180bec47d72ffbbd41ab699df39b1/pyobjc_framework_StoreKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:29269183e91043bbfee79851ae712073feba1e10845b8deeb7e6aaa20cfb3cf4", size = 11680, upload-time = "2025-01-14T19:00:35.36Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/7549a7bd2b068cd460792e09a66d88465aab2ac6fb2ddcf77b7bf5712eee/pyobjc_framework_storekit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:624105bd26a9ce5a097b3f96653e2700d33bb095828ed65ee0f4679b34d9f1e1", size = 11841, upload-time = "2025-06-14T20:55:29.735Z" }, + { url = "https://files.pythonhosted.org/packages/ac/61/6404aac6857ea43798882333bcc26bfd3c9c3a1efc7a575cbf3e53538e2a/pyobjc_framework_storekit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5ca3373272b6989917c88571ca170ce6d771180fe1a2b44c7643fe084569b93e", size = 11868, upload-time = "2025-06-14T20:55:30.454Z" }, ] [[package]] name = "pyobjc-framework-symbols" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/92/a20a3d7af3c99e0ea086e43715675160a04b86c1d069bdaeb3acdb015d92/pyobjc_framework_symbols-11.0.tar.gz", hash = "sha256:e3de7736dfb8107f515cfd23f03e874dd9468e88ab076d01d922a73fefb620fa", size = 13682, upload-time = "2025-01-14T19:05:45.727Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/af/7191276204bd3e7db1d0a3e490a869956606f77f7a303a04d92a5d0c3f7b/pyobjc_framework_symbols-11.1.tar.gz", hash = "sha256:0e09b7813ef2ebdca7567d3179807444dd60f3f393202b35b755d4e1baf99982", size = 13377, upload-time = "2025-06-14T20:58:38.542Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/ff/341d44f5347d48491682bece366444f3e230e33109266dcc6a17e6a7fc3d/pyobjc_framework_Symbols-11.0-py2.py3-none-any.whl", hash = "sha256:f1490823f40a8a540ac10628190695f27a717343914fe5db5fafa500f7c7bf44", size = 3263, upload-time = "2025-01-14T19:00:41.055Z" }, - { url = "https://files.pythonhosted.org/packages/94/a4/c21353872a2fc643206a44ac55b92b5b7533cdb2cb26c44a9048debc295a/pyobjc_framework_Symbols-11.0-py3-none-any.whl", hash = "sha256:0919e85fcf6f420f61d8d9a67cafa2ab4678666441ef4f001b31f5457900b314", size = 3335, upload-time = "2025-01-14T19:00:43.294Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6a/c91f64ef9b8cd20245b88e392c66cb2279c511724f4ea2983d92584d6f3e/pyobjc_framework_symbols-11.1-py2.py3-none-any.whl", hash = "sha256:1de6fc3af15fc8d5fd4869663a3250311844ec33e99ec8a1991a352ab61d641d", size = 3312, upload-time = "2025-06-14T20:55:35.456Z" }, ] [[package]] name = "pyobjc-framework-syncservices" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-coredata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/22/642186906f672461bab1d7773b35ef74e432b9789ca2248186b766e9fd3b/pyobjc_framework_syncservices-11.0.tar.gz", hash = "sha256:7867c23895a8289da8d56e962c144c36ed16bd101dc07d05281c55930b142471", size = 57453, upload-time = "2025-01-14T19:05:46.559Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/45/cd9fa83ed1d75be7130fb8e41c375f05b5d6621737ec37e9d8da78676613/pyobjc_framework_syncservices-11.1.tar.gz", hash = "sha256:0f141d717256b98c17ec2eddbc983c4bd39dfa00dc0c31b4174742e73a8447fe", size = 57996, upload-time = "2025-06-14T20:58:39.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/9b/484db4eed6b1e29e0d69275bd459ab21a6b3f98e8b2ce61beeb9971303ca/pyobjc_framework_SyncServices-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:89a398df6518cff1c63b7cccf3025e388f3ef299645734112c5aa1ac5f7ca30a", size = 13989, upload-time = "2025-01-14T19:00:45.216Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d8/dc86d708434b7cb59825c56549e64b118ba4b8584d2eb5a1514d1cd5d1bd/pyobjc_framework_SyncServices-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e870e82ed34c43607cc50dbae57a81dd419b75abc06670630cbbf41ae6e1402c", size = 14008, upload-time = "2025-01-14T19:00:46.188Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7e/60e184beafca85571cfa68d46a8f453a54edbc7d2eceb18163cfec438438/pyobjc_framework_syncservices-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bc6159bda4597149c6999b052a35ffd9fc4817988293da6e54a1e073fa571653", size = 13464, upload-time = "2025-06-14T20:55:37.117Z" }, + { url = "https://files.pythonhosted.org/packages/01/2b/6d7d65c08a9c51eed12eb7f83eaa48deaed621036f77221b3b0346c3f6c2/pyobjc_framework_syncservices-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:03124c8c7c7ce837f51e1c9bdcf84c6f1d5201f92c8a1c172ec34908d5e57415", size = 13496, upload-time = "2025-06-14T20:55:37.83Z" }, ] [[package]] name = "pyobjc-framework-systemconfiguration" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/70/ebebf311523f436df2407f35d7ce62482c01e530b77aceb3ca6356dcef43/pyobjc_framework_systemconfiguration-11.0.tar.gz", hash = "sha256:06487f0fdd43c6447b5fd3d7f3f59826178d32bcf74f848c5b3ea597191d471d", size = 142949, upload-time = "2025-01-14T19:05:47.466Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/3d/41590c0afc72e93d911348fbde0c9c1071ff53c6f86df42df64b21174bb9/pyobjc_framework_systemconfiguration-11.1.tar.gz", hash = "sha256:f30ed0e9a8233fecb06522e67795918ab230ddcc4a18e15494eff7532f4c3ae1", size = 143410, upload-time = "2025-06-14T20:58:39.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/8f/1b5f7e8e848d2c84204da08d5c63e42feff86b26cd508da7a4f95960b842/pyobjc_framework_SystemConfiguration-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:89d3c54abedcedbc2ce52c31ff4878251ca54a8535407ed6bd6584ce099c148b", size = 21836, upload-time = "2025-01-14T19:00:51.698Z" }, - { url = "https://files.pythonhosted.org/packages/6d/49/8660b3d0a46ac2f88e73cec3d10e21885b107f54635680ef0c677ac5cf3e/pyobjc_framework_SystemConfiguration-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8cbcb9662dbb5a034cfc5a44adaf2a0226a2985ae299a4ef4fd75bb49f30f5a0", size = 21727, upload-time = "2025-01-14T19:00:52.685Z" }, + { url = "https://files.pythonhosted.org/packages/64/9b/8fe26a9ac85898fa58f6206f357745ec44cd95b63786503ce05c382344ce/pyobjc_framework_systemconfiguration-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d12d5078611c905162bc951dffbb2a989b0dfd156952ba1884736c8dcbe38f7f", size = 21732, upload-time = "2025-06-14T20:55:43.951Z" }, + { url = "https://files.pythonhosted.org/packages/b9/61/0e9841bf1c7597f380a6dcefcc9335b6a909f20d9bdf07910cddc8552b42/pyobjc_framework_systemconfiguration-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6881929b828a566bf1349f09db4943e96a2b33f42556e1f7f6f28b192420f6fc", size = 21639, upload-time = "2025-06-14T20:55:44.678Z" }, ] [[package]] name = "pyobjc-framework-systemextensions" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/4b/904d818debf6216b7be009d492d998c819bf2f2791bfb75870a952e32cf9/pyobjc_framework_systemextensions-11.0.tar.gz", hash = "sha256:da293c99b428fb7f18a7a1d311b17177f73a20c7ffa94de3f72d760df924255e", size = 22531, upload-time = "2025-01-14T19:05:48.463Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/57/4609fd9183383616b1e643c2489ad774335f679523a974b9ce346a6d4d5b/pyobjc_framework_systemextensions-11.1.tar.gz", hash = "sha256:8ff9f0aad14dcdd07dd47545c1dd20df7a286306967b0a0232c81fcc382babe6", size = 23062, upload-time = "2025-06-14T20:58:40.686Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/3c/8f91b89554ef3127e037d90b3ef83c77a994bb889b7884a995756cd06b63/pyobjc_framework_SystemExtensions-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7a2ec417fa0d383cc066bc292541aa78fd2aec9cca83a98d41b7982f185d1f7", size = 8975, upload-time = "2025-01-14T19:00:57.822Z" }, - { url = "https://files.pythonhosted.org/packages/21/8c/cf2a018b5f1ecd216f8cb26a3b6fbe590d08de81a6c6b4658e001a203886/pyobjc_framework_SystemExtensions-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:62b99c6bd88bce642960fc2b9d5903fbfca680d16be9a4565a883eb4ba17ca5e", size = 8999, upload-time = "2025-01-14T19:00:58.865Z" }, + { url = "https://files.pythonhosted.org/packages/10/53/0fb6a200383fa98001ffa66b4f6344c68ccd092506699a353b30f18d7094/pyobjc_framework_systemextensions-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7e742ae51cdd86c0e609fe47189ea446de98d13b235b0a138a3f2e37e98cd359", size = 9125, upload-time = "2025-06-14T20:55:50.431Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/d9be444b39ec12d68b5e4f712b71d6c00d654936ff5744ea380c1bfabf06/pyobjc_framework_systemextensions-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3a2b1e84e4a118bfe13efb9f2888b065dc937e2a7e60afd4d0a82b51b8301a10", size = 9130, upload-time = "2025-06-14T20:55:51.127Z" }, ] [[package]] name = "pyobjc-framework-threadnetwork" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/17/fc8fde4eeb6697e0a5ba1a306cd62d3a95b53f3334744cd22b87037d8a14/pyobjc_framework_threadnetwork-11.0.tar.gz", hash = "sha256:f5713579380f6fb89c877796de86cb4e98428d7a9cbfebe566fb827ba23b2d8e", size = 13820, upload-time = "2025-01-14T19:05:49.307Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a4/5400a222ced0e4f077a8f4dd0188e08e2af4762e72ed0ed39f9d27feefc9/pyobjc_framework_threadnetwork-11.1.tar.gz", hash = "sha256:73a32782f44b61ca0f8a4a9811c36b1ca1cdcf96c8a3ba4de35d8e8e58a86ad5", size = 13572, upload-time = "2025-06-14T20:58:41.311Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/a9/908184da457e33a110de7d2d262efa69beaba6db243342df5654da03566b/pyobjc_framework_ThreadNetwork-11.0-py2.py3-none-any.whl", hash = "sha256:950d46a009cb992b12dbd8169a0450d8cc101fc982e03e6543078c6d7790e353", size = 3700, upload-time = "2025-01-14T19:01:03.254Z" }, - { url = "https://files.pythonhosted.org/packages/59/d4/4694fc7a627d2b6b37c51433ba7f02a39a283a445dc77349b82fe24534f1/pyobjc_framework_ThreadNetwork-11.0-py3-none-any.whl", hash = "sha256:1218649e4f488ca411af13b74f1dee1e7a178169e0f5963342ba8a7c46037ea7", size = 3770, upload-time = "2025-01-14T19:01:05.456Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f0/b7a577d00bdb561efef82b046a75f627a60de53566ab2d9e9ddd5bd11b66/pyobjc_framework_threadnetwork-11.1-py2.py3-none-any.whl", hash = "sha256:55021455215a0d3ad4e40152f94154e29062e73655558c5f6e71ab097d90083e", size = 3751, upload-time = "2025-06-14T20:55:55.643Z" }, ] [[package]] name = "pyobjc-framework-uniformtypeidentifiers" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/4f/fd571c1f87d5ee3d86c4d2008806e9623d2662bbc788d9001b3fff35275f/pyobjc_framework_uniformtypeidentifiers-11.0.tar.gz", hash = "sha256:6ae6927a3ed1f0197a8c472226f11f46ccd5ed398b4449613e1d10346d9ed15d", size = 20860, upload-time = "2025-01-14T19:05:50.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/4f/066ed1c69352ccc29165f45afb302f8c9c2b5c6f33ee3abfa41b873c07e5/pyobjc_framework_uniformtypeidentifiers-11.1.tar.gz", hash = "sha256:86c499bec8953aeb0c95af39b63f2592832384f09f12523405650b5d5f1ed5e9", size = 20599, upload-time = "2025-06-14T20:58:41.945Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/f2/094888af07fb7f0443996e5d91915e74b87e8705b599b68b516a0e94a63d/pyobjc_framework_UniformTypeIdentifiers-11.0-py2.py3-none-any.whl", hash = "sha256:acffb86e8b03b66c49274236b3df3a254cacd32b9f25bd7a5bd59baaaf738624", size = 4841, upload-time = "2025-01-14T19:01:06.404Z" }, - { url = "https://files.pythonhosted.org/packages/88/9c/4cc0522cc546e6a3bf8a921e3a9f0ed078e3cf907d616760d9f3d7754919/pyobjc_framework_UniformTypeIdentifiers-11.0-py3-none-any.whl", hash = "sha256:a3097f186c7e231b19218a3ceecb3b70a8f2b2e9e642ef409dc7a195a30c869e", size = 4910, upload-time = "2025-01-14T19:01:07.393Z" }, + { url = "https://files.pythonhosted.org/packages/de/3b/b63b8137dd9f455d5abece6702c06c6b613fac6fda1319aaa2f79d00c380/pyobjc_framework_uniformtypeidentifiers-11.1-py2.py3-none-any.whl", hash = "sha256:6e2e8ea89eb8ca03bc2bc8e506fff901e71d916276475c8d81fbf0280059cb4c", size = 4891, upload-time = "2025-06-14T20:55:56.432Z" }, ] [[package]] name = "pyobjc-framework-usernotifications" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/f5/ca3e6a7d940b3aca4323e4f5409b14b5d2eb45432158430c584e3800ce4d/pyobjc_framework_usernotifications-11.0.tar.gz", hash = "sha256:7950a1c6a8297f006c26c3d286705ffc2a07061d6e844f1106290572097b872c", size = 54857, upload-time = "2025-01-14T19:05:52.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/4c/e7e180fcd06c246c37f218bcb01c40ea0213fde5ace3c09d359e60dcaafd/pyobjc_framework_usernotifications-11.1.tar.gz", hash = "sha256:38fc763afa7854b41ddfca8803f679a7305d278af8a7ad02044adc1265699996", size = 55428, upload-time = "2025-06-14T20:58:42.572Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/bf/5545d5c9d0d10a603ad406a5ce727de6a47daace9c38d4484818611599f3/pyobjc_framework_UserNotifications-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4bf78fa37f574f5b43db9b83ca02e82ab45803589f970042afdcd1cb8c01396d", size = 9483, upload-time = "2025-01-14T19:01:09.256Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/41f4d18120b2c006f756edde1845a2df45fdbd6957e540f8ebcfae25747f/pyobjc_framework_UserNotifications-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0b4c06c3862405e103e964327581c28e5390a2d4cd0cef3d8e64afda03c9f431", size = 9506, upload-time = "2025-01-14T19:01:10.218Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bb/ae9c9301a86b7c0c26583c59ac761374cb6928c3d34cae514939e93e44b1/pyobjc_framework_usernotifications-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7140d337dd9dc3635add2177086429fdd6ef24970935b22fffdc5ec7f02ebf60", size = 9599, upload-time = "2025-06-14T20:55:58.051Z" }, + { url = "https://files.pythonhosted.org/packages/03/af/a54e343a7226dc65a65f7a561c060f8c96cb9f92f41ce2242d20d82ae594/pyobjc_framework_usernotifications-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ce6006989fd4a59ec355f6797ccdc9946014ea5241ff7875854799934dbba901", size = 9606, upload-time = "2025-06-14T20:55:59.088Z" }, ] [[package]] name = "pyobjc-framework-usernotificationsui" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, { name = "pyobjc-framework-usernotifications" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e8/f0d50cdc678260a628b92e55b5752155f941c2f72b96fe3f2412a28c5d79/pyobjc_framework_usernotificationsui-11.0.tar.gz", hash = "sha256:d0ec597d189b4d228b0b836474aef318652c1c287b33442a1403c49dc59fdb7f", size = 14369, upload-time = "2025-01-14T19:05:54.498Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/c4/03d97bd3adcee9b857533cb42967df0d019f6a034adcdbcfca2569d415b2/pyobjc_framework_usernotificationsui-11.1.tar.gz", hash = "sha256:18e0182bddd10381884530d6a28634ebb3280912592f8f2ad5bac2a9308c6a65", size = 14123, upload-time = "2025-06-14T20:58:43.267Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/f7/64c95c6f82e92bb1cbcb8d5c3658c79c954668627eef28f11e76025a3ed1/pyobjc_framework_UserNotificationsUI-11.0-py2.py3-none-any.whl", hash = "sha256:6185d9c9513b6a823cd72dcf40d2fb33bbf0f2c9a98528e0e112580b47ac3632", size = 3856, upload-time = "2025-01-14T19:01:15.43Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c3/e1d64c9e523b5192e0179b6723ee465e74d6c282104a49a67347d527a65d/pyobjc_framework_UserNotificationsUI-11.0-py3-none-any.whl", hash = "sha256:e4439e549265929ddad1feca7b062d00c2d3732470f349cb0d594705e0257919", size = 3932, upload-time = "2025-01-14T19:01:16.486Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2c/0bb489b5ac4daf83b113018701ce30a0cb4bf47c615c92c5844a16e0a012/pyobjc_framework_usernotificationsui-11.1-py2.py3-none-any.whl", hash = "sha256:b84d73d90ab319acf8fad5c59b7a5e2b6023fbb2efd68c58b532e3b3b52f647a", size = 3914, upload-time = "2025-06-14T20:56:03.978Z" }, ] [[package]] name = "pyobjc-framework-videosubscriberaccount" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/2e/6a7debd84911a9384b4e7a9cc3f308e3461a00a9d74f33b153bdd872f15f/pyobjc_framework_videosubscriberaccount-11.0.tar.gz", hash = "sha256:163b32f361f48b9d20f317461464abd4427b3242693ae011633fc443c7d5449c", size = 29100, upload-time = "2025-01-14T19:05:55.319Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/00/cd9d93d06204bbb7fe68fb97022b0dd4ecdf8af3adb6d70a41e22c860d55/pyobjc_framework_videosubscriberaccount-11.1.tar.gz", hash = "sha256:2dd78586260fcee51044e129197e8bf2e157176e02babeec2f873afa4235d8c6", size = 28856, upload-time = "2025-06-14T20:58:43.903Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/82/94650fe5cc68c0c32fe56fe22cd7eb2874b28f987a9e259fac12cbea7705/pyobjc_framework_VideoSubscriberAccount-11.0-py2.py3-none-any.whl", hash = "sha256:1deec8d5a0138ae51b5ca7bfb7f6fe1b0dc3cbb52db3111059708efa5f8a8d04", size = 4637, upload-time = "2025-01-14T19:01:17.365Z" }, - { url = "https://files.pythonhosted.org/packages/61/54/1765507adad1b0c9bc6be10f09b249d425212bc0d9fef1efdfd872ee9807/pyobjc_framework_VideoSubscriberAccount-11.0-py3-none-any.whl", hash = "sha256:0095eddb5fc942f9e049bc4c683cf28c77ea60c60942552c3c48bf74c8fdca9b", size = 4709, upload-time = "2025-01-14T19:01:18.349Z" }, + { url = "https://files.pythonhosted.org/packages/4b/dc/b409dee6dd58a5db2e9a681bde8894c9715468689f18e040f7d252794c3d/pyobjc_framework_videosubscriberaccount-11.1-py2.py3-none-any.whl", hash = "sha256:d5a95ae9f2a6f0180a5bbb10e76c064f0fd327aae00a2fe90aa7b65ed4dad7ef", size = 4695, upload-time = "2025-06-14T20:56:06.027Z" }, ] [[package]] name = "pyobjc-framework-videotoolbox" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, @@ -4211,29 +4154,29 @@ dependencies = [ { name = "pyobjc-framework-coremedia" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/2d/c031a132b142fcd20846cc1ac3ba92abaa58ec04164fd36ca978d9374f1c/pyobjc_framework_videotoolbox-11.0.tar.gz", hash = "sha256:a54ed8f8bcbdd2bdea2a296dc02a8a7d42f81e2b6ccbf4d1f10cec5e7a09bec0", size = 81157, upload-time = "2025-01-14T19:05:56.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/e3/df9096f54ae1f27cab8f922ee70cbda5d80f8c1d12734c38580829858133/pyobjc_framework_videotoolbox-11.1.tar.gz", hash = "sha256:a27985656e1b639cdb102fcc727ebc39f71bb1a44cdb751c8c80cc9fe938f3a9", size = 88551, upload-time = "2025-06-14T20:58:44.566Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/ae/ff697840bdcf3530e8fba84e2a606813eda1ee90be074f12e2857460cebf/pyobjc_framework_VideoToolbox-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:12af56190e65c3b60c6ca14fe69045e5ffb5908ea1363580506eb32603b80855", size = 13446, upload-time = "2025-01-14T19:01:21.066Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ef/9e7230435da47016983a3c9ea7b1d5237b43fce2d8b2b923eb638b7694f5/pyobjc_framework_VideoToolbox-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4ed7f073bd8dfecca0da6359d5cd871b2f39144883930bddd41ca818447de608", size = 13451, upload-time = "2025-01-14T19:01:22.009Z" }, + { url = "https://files.pythonhosted.org/packages/0b/41/fda951f1c734a68d7bf46ecc03bfff376a690ad771029c4289ba0423a52e/pyobjc_framework_videotoolbox-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:94c17bffe0f4692db2e7641390dfdcd0f73ddbb0afa6c81ef504219be0777930", size = 17325, upload-time = "2025-06-14T20:56:07.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/cf/569babadbf1f9598f62c400ee02da19d4ab5f36276978c81080999399df9/pyobjc_framework_videotoolbox-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c55285c3c78183fd2a092d582e30b562777a82985cccca9e7e99a0aff2601591", size = 17432, upload-time = "2025-06-14T20:56:08.457Z" }, ] [[package]] name = "pyobjc-framework-virtualization" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/8d/e57e1f2c5ac950dc3da6c977effde4a55b8b70424b1bdb97b5530559f5bc/pyobjc_framework_virtualization-11.0.tar.gz", hash = "sha256:03e1c1fa20950aa7c275e5f11f1257108b6d1c6a7403afb86f4e9d5fae87b73c", size = 78144, upload-time = "2025-01-14T19:05:57.086Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/ff/57214e8f42755eeaad516a7e673dae4341b8742005d368ecc22c7a790b0b/pyobjc_framework_virtualization-11.1.tar.gz", hash = "sha256:4221ee5eb669e43a2ff46e04178bec149af2d65205deb5d4db5fa62ea060e022", size = 78633, upload-time = "2025-06-14T20:58:45.358Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/c9/b2f8322d7ced14822270481be5b44f1846aa7c09b4b3cb52517dc1054f4b/pyobjc_framework_Virtualization-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:334712792136ffcf3c63a63cea01ce33d60309a82721c95e25f0cc26b95f72cc", size = 13417, upload-time = "2025-01-14T19:01:28.821Z" }, - { url = "https://files.pythonhosted.org/packages/1e/96/d64425811a4ef2c8b38914ea1a91bbd2aa6136bb79989e4821acd6d28e67/pyobjc_framework_Virtualization-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b848b1ab365906b11a507c8146e477c27d2bf56159d49d21fda15b93c2811ec", size = 13430, upload-time = "2025-01-14T19:01:29.733Z" }, + { url = "https://files.pythonhosted.org/packages/64/8b/5eeabfd08d5e6801010496969c1b67517bbda348ff0578ca5f075aa58926/pyobjc_framework_virtualization-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c2a812da4c995e1f8076678130d0b0a63042aa48219f8fb43b70e13eabcbdbc2", size = 13054, upload-time = "2025-06-14T20:56:13.866Z" }, + { url = "https://files.pythonhosted.org/packages/c8/4f/fe1930f4ce2c7d2f4c34bb53adf43f412bc91364e8e4cb450a7c8a6b8b59/pyobjc_framework_virtualization-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:59df6702b3e63200752be7d9c0dc590cb4c3b699c886f9a8634dd224c74b3c3c", size = 13084, upload-time = "2025-06-14T20:56:14.617Z" }, ] [[package]] name = "pyobjc-framework-vision" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, @@ -4241,24 +4184,24 @@ dependencies = [ { name = "pyobjc-framework-coreml" }, { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/53/dc2e0562a177af9306efceb84bc21f5cf7470acaa8f28f64e62bf828b7e1/pyobjc_framework_vision-11.0.tar.gz", hash = "sha256:45342e5253c306dbcd056a68bff04ffbfa00e9ac300a02aabf2e81053b771e39", size = 133175, upload-time = "2025-01-14T19:05:58.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/a8/7128da4d0a0103cabe58910a7233e2f98d18c590b1d36d4b3efaaedba6b9/pyobjc_framework_vision-11.1.tar.gz", hash = "sha256:26590512ee7758da3056499062a344b8a351b178be66d4b719327884dde4216b", size = 133721, upload-time = "2025-06-14T20:58:46.095Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/84/d23a745d46858409a1dca3e7f5cb3089c148ebb8d42e7a6289e1972ad650/pyobjc_framework_Vision-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ca7cc48332d804a02b5b17f31bed52dd4b7c323f9e4ff4b4e7ecd35d39cc0759", size = 21754, upload-time = "2025-01-14T19:01:35.504Z" }, - { url = "https://files.pythonhosted.org/packages/3a/80/6db9fc2a3f8b991860156f4700f979ad8aa1e9617b0efa720ee3b52e3602/pyobjc_framework_Vision-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1b07aa867dda47d2a4883cd969e248039988b49190ba097cbe9747156b5d1f30", size = 17099, upload-time = "2025-01-14T19:01:37.457Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/a745a5491d7af6034ac9e0d627e7b41b42978df0a469b86cdf372ba8917f/pyobjc_framework_vision-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bfbde43c9d4296e1d26548b6d30ae413e2029425968cd8bce96d3c5a735e8f2c", size = 21657, upload-time = "2025-06-14T20:56:20.265Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b5/54c0227a695557ea3065bc035b20a5c256f6f3b861e095eee1ec4b4d8cee/pyobjc_framework_vision-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df076c3e3e672887182953efc934c1f9683304737e792ec09a29bfee90d2e26a", size = 16829, upload-time = "2025-06-14T20:56:21.355Z" }, ] [[package]] name = "pyobjc-framework-webkit" -version = "11.0" +version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyobjc-core" }, { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/4f/02a6270acf225c2a34339677e796002c77506238475059ae6e855358a40c/pyobjc_framework_webkit-11.0.tar.gz", hash = "sha256:fa6bedf9873786b3376a74ce2ea9dcd311f2a80f61e33dcbd931cc956aa29644", size = 767210, upload-time = "2025-01-14T19:05:59.3Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/04/fb3d0b68994f7e657ef00c1ac5fc1c04ae2fc7ea581d647f5ae1f6739b14/pyobjc_framework_webkit-11.1.tar.gz", hash = "sha256:27e701c7aaf4f24fc7e601a128e2ef14f2773f4ab071b9db7438dc5afb5053ae", size = 717102, upload-time = "2025-06-14T20:58:47.461Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/63/6f04faa75c4c39c54007b256a8e13838c1de213d487f561937d342ec2eac/pyobjc_framework_WebKit-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:163abaa5a665b59626ef20cdc3dcc5e2e3fcd9830d5fc328507e13f663acd0ed", size = 44940, upload-time = "2025-01-14T19:01:44.396Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/934f03510e7f49454fbf6eeff8ad2eca5d8bfbe71aa4b8a034f8132af2fa/pyobjc_framework_WebKit-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2e4911519e94822011d99fdb9addf4a176f45a79808dab18dc303293f4590f7c", size = 44901, upload-time = "2025-01-14T19:01:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b6/d62c01a83c22619edf2379a6941c9f6b7aee01c565b9c1170696f85cba95/pyobjc_framework_webkit-11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:10ec89d727af8f216ba5911ff5553f84a5b660f5ddf75b07788e3a439c281165", size = 51406, upload-time = "2025-06-14T20:56:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7e/fa2c18c0c0f9321e5036e54b9da7a196956b531e50fe1a76e7dfdbe8fac2/pyobjc_framework_webkit-11.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1a6e6f64ca53c4953f17e808ecac11da288d9a6ade738156ba161732a5e0c96a", size = 51464, upload-time = "2025-06-14T20:56:27.653Z" }, ] [[package]] @@ -4343,42 +4286,44 @@ wheels = [ [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] [[package]] name = "pytest-asyncio" -version = "0.26.0" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, ] [[package]] name = "pytest-cov" -version = "6.1.1" +version = "6.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] [[package]] @@ -4395,14 +4340,14 @@ wheels = [ [[package]] name = "pytest-mock" -version = "3.14.0" +version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814, upload-time = "2024-03-21T22:14:04.964Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863, upload-time = "2024-03-21T22:14:02.694Z" }, + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, ] [[package]] @@ -4431,15 +4376,15 @@ wheels = [ [[package]] name = "pytest-subtests" -version = "0.14.1" +version = "0.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/4c/ba9eab21a2250c2d46c06c0e3cd316850fde9a90da0ac8d0202f074c6817/pytest_subtests-0.14.1.tar.gz", hash = "sha256:350c00adc36c3aff676a66135c81aed9e2182e15f6c3ec8721366918bbbf7580", size = 17632, upload-time = "2024-12-10T00:21:04.856Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/30/6ec8dfc678ddfd1c294212bbd7088c52d3f7fbf3f05e6d8a440c13b9741a/pytest_subtests-0.14.2.tar.gz", hash = "sha256:7154a8665fd528ee70a76d00216a44d139dc3c9c83521a0f779f7b0ad4f800de", size = 18083, upload-time = "2025-06-13T10:50:01.636Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/b7/7ca948d35642ae72500efda6ba6fa61dcb6683feb596d19c4747c63c0789/pytest_subtests-0.14.1-py3-none-any.whl", hash = "sha256:e92a780d98b43118c28a16044ad9b841727bd7cb6a417073b38fd2d7ccdf052d", size = 8833, upload-time = "2024-12-10T00:20:58.873Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/9bf12e59fb882b0cf4f993871e1adbee094802224c429b00861acee1a169/pytest_subtests-0.14.2-py3-none-any.whl", hash = "sha256:8da0787c994ab372a13a0ad7d390533ad2e4385cac167b3ac501258c885d0b66", size = 9115, upload-time = "2025-06-13T10:50:00.543Z" }, ] [[package]] @@ -4456,16 +4401,12 @@ wheels = [ [[package]] name = "pytest-xdist" -version = "3.6.1" -source = { registry = "https://pypi.org/simple" } +version = "3.7.1.dev3+g909e97b" +source = { git = "https://github.com/sshane/pytest-xdist?rev=909e97b49d12401c10608f9d777bfc9dab8a4413#909e97b49d12401c10608f9d777bfc9dab8a4413" } dependencies = [ { name = "execnet" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/c4/3c310a19bc1f1e9ef50075582652673ef2bfc8cd62afef9585683821902f/pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d", size = 84060, upload-time = "2024-04-28T19:29:54.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108, upload-time = "2024-04-28T19:29:52.813Z" }, -] [[package]] name = "python-dateutil" @@ -4602,40 +4543,50 @@ wheels = [ [[package]] name = "pyzmq" -version = "26.4.0" +version = "27.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/11/b9213d25230ac18a71b39b3723494e57adebe36e066397b961657b3b41c1/pyzmq-26.4.0.tar.gz", hash = "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d", size = 278293, upload-time = "2025-04-04T12:05:44.049Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/6d/234e3b0aa82fd0290b1896e9992f56bdddf1f97266110be54d0177a9d2d9/pyzmq-26.4.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:bfcf82644c9b45ddd7cd2a041f3ff8dce4a0904429b74d73a439e8cab1bd9e54", size = 1339723, upload-time = "2025-04-04T12:03:24.358Z" }, - { url = "https://files.pythonhosted.org/packages/4f/11/6d561efe29ad83f7149a7cd48e498e539ed09019c6cd7ecc73f4cc725028/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9bcae3979b2654d5289d3490742378b2f3ce804b0b5fd42036074e2bf35b030", size = 672645, upload-time = "2025-04-04T12:03:25.693Z" }, - { url = "https://files.pythonhosted.org/packages/19/fd/81bfe3e23f418644660bad1a90f0d22f0b3eebe33dd65a79385530bceb3d/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccdff8ac4246b6fb60dcf3982dfaeeff5dd04f36051fe0632748fc0aa0679c01", size = 910133, upload-time = "2025-04-04T12:03:27.625Z" }, - { url = "https://files.pythonhosted.org/packages/97/68/321b9c775595ea3df832a9516252b653fe32818db66fdc8fa31c9b9fce37/pyzmq-26.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4550af385b442dc2d55ab7717837812799d3674cb12f9a3aa897611839c18e9e", size = 867428, upload-time = "2025-04-04T12:03:29.004Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6e/159cbf2055ef36aa2aa297e01b24523176e5b48ead283c23a94179fb2ba2/pyzmq-26.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f7ffe9db1187a253fca95191854b3fda24696f086e8789d1d449308a34b88", size = 862409, upload-time = "2025-04-04T12:03:31.032Z" }, - { url = "https://files.pythonhosted.org/packages/05/1c/45fb8db7be5a7d0cadea1070a9cbded5199a2d578de2208197e592f219bd/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3709c9ff7ba61589b7372923fd82b99a81932b592a5c7f1a24147c91da9a68d6", size = 1205007, upload-time = "2025-04-04T12:03:32.687Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fa/658c7f583af6498b463f2fa600f34e298e1b330886f82f1feba0dc2dd6c3/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f8f3c30fb2d26ae5ce36b59768ba60fb72507ea9efc72f8f69fa088450cff1df", size = 1514599, upload-time = "2025-04-04T12:03:34.084Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d7/44d641522353ce0a2bbd150379cb5ec32f7120944e6bfba4846586945658/pyzmq-26.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:382a4a48c8080e273427fc692037e3f7d2851959ffe40864f2db32646eeb3cef", size = 1414546, upload-time = "2025-04-04T12:03:35.478Z" }, - { url = "https://files.pythonhosted.org/packages/72/76/c8ed7263218b3d1e9bce07b9058502024188bd52cc0b0a267a9513b431fc/pyzmq-26.4.0-cp311-cp311-win32.whl", hash = "sha256:d56aad0517d4c09e3b4f15adebba8f6372c5102c27742a5bdbfc74a7dceb8fca", size = 579247, upload-time = "2025-04-04T12:03:36.846Z" }, - { url = "https://files.pythonhosted.org/packages/c3/d0/2d9abfa2571a0b1a67c0ada79a8aa1ba1cce57992d80f771abcdf99bb32c/pyzmq-26.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:963977ac8baed7058c1e126014f3fe58b3773f45c78cce7af5c26c09b6823896", size = 644727, upload-time = "2025-04-04T12:03:38.578Z" }, - { url = "https://files.pythonhosted.org/packages/0d/d1/c8ad82393be6ccedfc3c9f3adb07f8f3976e3c4802640fe3f71441941e70/pyzmq-26.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0c8e8cadc81e44cc5088fcd53b9b3b4ce9344815f6c4a03aec653509296fae3", size = 559942, upload-time = "2025-04-04T12:03:40.143Z" }, - { url = "https://files.pythonhosted.org/packages/10/44/a778555ebfdf6c7fc00816aad12d185d10a74d975800341b1bc36bad1187/pyzmq-26.4.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5227cb8da4b6f68acfd48d20c588197fd67745c278827d5238c707daf579227b", size = 1341586, upload-time = "2025-04-04T12:03:41.954Z" }, - { url = "https://files.pythonhosted.org/packages/9c/4f/f3a58dc69ac757e5103be3bd41fb78721a5e17da7cc617ddb56d973a365c/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1c07a7fa7f7ba86554a2b1bef198c9fed570c08ee062fd2fd6a4dcacd45f905", size = 665880, upload-time = "2025-04-04T12:03:43.45Z" }, - { url = "https://files.pythonhosted.org/packages/fe/45/50230bcfb3ae5cb98bee683b6edeba1919f2565d7cc1851d3c38e2260795/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae775fa83f52f52de73183f7ef5395186f7105d5ed65b1ae65ba27cb1260de2b", size = 902216, upload-time = "2025-04-04T12:03:45.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/59/56bbdc5689be5e13727491ad2ba5efd7cd564365750514f9bc8f212eef82/pyzmq-26.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c760d0226ebd52f1e6b644a9e839b5db1e107a23f2fcd46ec0569a4fdd4e63", size = 859814, upload-time = "2025-04-04T12:03:47.188Z" }, - { url = "https://files.pythonhosted.org/packages/81/b1/57db58cfc8af592ce94f40649bd1804369c05b2190e4cbc0a2dad572baeb/pyzmq-26.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ef8c6ecc1d520debc147173eaa3765d53f06cd8dbe7bd377064cdbc53ab456f5", size = 855889, upload-time = "2025-04-04T12:03:49.223Z" }, - { url = "https://files.pythonhosted.org/packages/e8/92/47542e629cbac8f221c230a6d0f38dd3d9cff9f6f589ed45fdf572ffd726/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3150ef4084e163dec29ae667b10d96aad309b668fac6810c9e8c27cf543d6e0b", size = 1197153, upload-time = "2025-04-04T12:03:50.591Z" }, - { url = "https://files.pythonhosted.org/packages/07/e5/b10a979d1d565d54410afc87499b16c96b4a181af46e7645ab4831b1088c/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4448c9e55bf8329fa1dcedd32f661bf611214fa70c8e02fee4347bc589d39a84", size = 1507352, upload-time = "2025-04-04T12:03:52.473Z" }, - { url = "https://files.pythonhosted.org/packages/ab/58/5a23db84507ab9c01c04b1232a7a763be66e992aa2e66498521bbbc72a71/pyzmq-26.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e07dde3647afb084d985310d067a3efa6efad0621ee10826f2cb2f9a31b89d2f", size = 1406834, upload-time = "2025-04-04T12:03:54Z" }, - { url = "https://files.pythonhosted.org/packages/22/74/aaa837b331580c13b79ac39396601fb361454ee184ca85e8861914769b99/pyzmq-26.4.0-cp312-cp312-win32.whl", hash = "sha256:ba034a32ecf9af72adfa5ee383ad0fd4f4e38cdb62b13624278ef768fe5b5b44", size = 577992, upload-time = "2025-04-04T12:03:55.815Z" }, - { url = "https://files.pythonhosted.org/packages/30/0f/55f8c02c182856743b82dde46b2dc3e314edda7f1098c12a8227eeda0833/pyzmq-26.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:056a97aab4064f526ecb32f4343917a4022a5d9efb6b9df990ff72e1879e40be", size = 640466, upload-time = "2025-04-04T12:03:57.231Z" }, - { url = "https://files.pythonhosted.org/packages/e4/29/073779afc3ef6f830b8de95026ef20b2d1ec22d0324d767748d806e57379/pyzmq-26.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f23c750e485ce1eb639dbd576d27d168595908aa2d60b149e2d9e34c9df40e0", size = 556342, upload-time = "2025-04-04T12:03:59.218Z" }, - { url = "https://files.pythonhosted.org/packages/04/52/a70fcd5592715702248306d8e1729c10742c2eac44529984413b05c68658/pyzmq-26.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4478b14cb54a805088299c25a79f27eaf530564a7a4f72bf432a040042b554eb", size = 834405, upload-time = "2025-04-04T12:05:13.3Z" }, - { url = "https://files.pythonhosted.org/packages/25/f9/1a03f1accff16b3af1a6fa22cbf7ced074776abbf688b2e9cb4629700c62/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a28ac29c60e4ba84b5f58605ace8ad495414a724fe7aceb7cf06cd0598d04e1", size = 569578, upload-time = "2025-04-04T12:05:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/76/0c/3a633acd762aa6655fcb71fa841907eae0ab1e8582ff494b137266de341d/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43b03c1ceea27c6520124f4fb2ba9c647409b9abdf9a62388117148a90419494", size = 798248, upload-time = "2025-04-04T12:05:17.376Z" }, - { url = "https://files.pythonhosted.org/packages/cd/cc/6c99c84aa60ac1cc56747bed6be8ce6305b9b861d7475772e7a25ce019d3/pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7731abd23a782851426d4e37deb2057bf9410848a4459b5ede4fe89342e687a9", size = 756757, upload-time = "2025-04-04T12:05:19.19Z" }, - { url = "https://files.pythonhosted.org/packages/13/9c/d8073bd898eb896e94c679abe82e47506e2b750eb261cf6010ced869797c/pyzmq-26.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a222ad02fbe80166b0526c038776e8042cd4e5f0dec1489a006a1df47e9040e0", size = 555371, upload-time = "2025-04-04T12:05:20.702Z" }, + { 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" }, +] + +[[package]] +name = "qrcode" +version = "8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" }, ] [[package]] @@ -4664,7 +4615,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -4672,14 +4623,14 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] [[package]] name = "rerun-sdk" -version = "0.23.2" +version = "0.23.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -4689,23 +4640,23 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/24/98/ee9acc4ac36e977bcacd3d65704127026a02228f4f5efa9cfd4243dd2036/rerun_sdk-0.23.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:fb9f0279a8b87f64bc29ed2c4c3cbb602e36b9b373f691ff657968ed6009c5c3", size = 66816731, upload-time = "2025-05-06T15:54:46.177Z" }, - { url = "https://files.pythonhosted.org/packages/3a/84/6164958c9b4dcd9b9dab16e1adbf4cea4339f52ce212c084b57bc0fd1a05/rerun_sdk-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:02d99f48659abeca86fcbd56498d30ab0c4b34b8a0732b02324c3fe79830cf6f", size = 62697629, upload-time = "2025-05-06T15:54:52.083Z" }, - { url = "https://files.pythonhosted.org/packages/c0/4d/1b9dc66a5827f25f9ce5fe9b719f97f42403d1a2276e765082d224590dbe/rerun_sdk-0.23.2-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:fdc844eddb6dcc924e48b3cd8dd88ed8b70751301d386175dd2fd7c9594580b3", size = 68935354, upload-time = "2025-05-06T15:54:56.27Z" }, - { url = "https://files.pythonhosted.org/packages/e7/7b/16b9bcd67b3dea1842d556100ce2e0b5c6bd9ad7943aeab2a76bae4b2be5/rerun_sdk-0.23.2-cp39-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:dac599ef5d5563f4ebc90c20f09b77c458bac4df845e178112257f79f3eef579", size = 72209426, upload-time = "2025-05-06T15:55:00.641Z" }, - { url = "https://files.pythonhosted.org/packages/67/54/ecb761555f6fbd4e32c384ba5a8424c761925be54259708093205c916c64/rerun_sdk-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:ce3f9adfb04df79f30ca1e6deb997b021af87878672f79dfa3543a4f6a83491f", size = 58180134, upload-time = "2025-05-06T15:55:06.284Z" }, + { url = "https://files.pythonhosted.org/packages/00/54/710685ea836d91f14422de515262a9db5c2a17777e59ae4c1c19b46c1fa0/rerun_sdk-0.23.3-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8a048ace69443aeb4040046d4bc28ba17be2c6862df7c2161e21b0608d5d6e0f", size = 59486089, upload-time = "2025-05-26T16:08:08.395Z" }, + { url = "https://files.pythonhosted.org/packages/26/81/b94c23cde1b9ed54900f47e337f8e156d9fdec7494eaa11bbb592c26a5d9/rerun_sdk-0.23.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a3ef05acfa9315d956c69eb680a50e46c802ba27eb8025bab71e14a989299383", size = 55128863, upload-time = "2025-05-26T16:08:13.328Z" }, + { url = "https://files.pythonhosted.org/packages/02/bc/c32f71969a5d981087fded194012ef162391e49dba71ba5115a17c642065/rerun_sdk-0.23.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:0cc9c60f1180edca1a4d70dbe591eb108e4dbe599c31136949b8d4106410cd14", size = 61603766, upload-time = "2025-05-26T16:08:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ef/93b6a8932747842e5f7c27f3ce7ef82bbc046aad3b7d5b2e905ffc5614a7/rerun_sdk-0.23.3-cp39-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:49833bf3a4854eaf287a8bfe7dfa4cd8f67b0fd7917a469c8bb7061895fb1cf8", size = 64875203, upload-time = "2025-05-26T16:08:21.95Z" }, + { url = "https://files.pythonhosted.org/packages/c5/36/759287d69301a3ada86ee102369a9521dfef0bb91b215e98a6a7b262a311/rerun_sdk-0.23.3-cp39-abi3-win_amd64.whl", hash = "sha256:88639632116b3761e2a45f4e5ac39eaf3406b615f516baccb508d93c6b42cc4e", size = 50862558, upload-time = "2025-05-26T16:08:26.98Z" }, ] [[package]] name = "ruamel-yaml" -version = "0.18.10" +version = "0.18.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447, upload-time = "2025-01-06T14:08:51.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/87/6da0df742a4684263261c253f00edd5829e6aca970fff69e75028cccc547/ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7", size = 145511, upload-time = "2025-06-09T08:51:09.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729, upload-time = "2025-01-06T14:08:47.471Z" }, + { url = "https://files.pythonhosted.org/packages/af/6d/6fe4805235e193aad4aaf979160dd1f3c487c57d48b810c816e6e842171b/ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2", size = 118570, upload-time = "2025-06-09T08:51:06.348Z" }, ] [[package]] @@ -4736,36 +4687,36 @@ wheels = [ [[package]] name = "rubicon-objc" -version = "0.5.0" +version = "0.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/13/586c9baa985eae0f718029506b40ca41295d51a546567414b2bcf8ccacef/rubicon_objc-0.5.0.tar.gz", hash = "sha256:18f075649780d95df53d483642068c767d7d2cfbbf075ddef124a44b40b6d92e", size = 173652, upload-time = "2025-01-07T00:25:10.491Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/30/5b2407b8762ed882e5732e19c485b9ea2f07d35462615a3212638bab66c2/rubicon_objc-0.5.0-py3-none-any.whl", hash = "sha256:a9c2a605120d6e5be327d3f42a71b60963125987e116f51846757b5e110854fa", size = 62711, upload-time = "2025-01-07T00:25:08.959Z" }, + { 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" }, ] [[package]] name = "ruff" -version = "0.11.10" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/4c/4a3c5a97faaae6b428b336dcca81d03ad04779f8072c267ad2bd860126bf/ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6", size = 4165632, upload-time = "2025-05-15T14:08:56.76Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/9f/596c628f8824a2ce4cd12b0f0b4c0629a62dfffc5d0f742c19a1d71be108/ruff-0.11.10-py3-none-linux_armv6l.whl", hash = "sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58", size = 10316243, upload-time = "2025-05-15T14:08:12.884Z" }, - { url = "https://files.pythonhosted.org/packages/3c/38/c1e0b77ab58b426f8c332c1d1d3432d9fc9a9ea622806e208220cb133c9e/ruff-0.11.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed", size = 11083636, upload-time = "2025-05-15T14:08:16.551Z" }, - { url = "https://files.pythonhosted.org/packages/23/41/b75e15961d6047d7fe1b13886e56e8413be8467a4e1be0a07f3b303cd65a/ruff-0.11.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca", size = 10441624, upload-time = "2025-05-15T14:08:19.032Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2c/e396b6703f131406db1811ea3d746f29d91b41bbd43ad572fea30da1435d/ruff-0.11.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2", size = 10624358, upload-time = "2025-05-15T14:08:21.542Z" }, - { url = "https://files.pythonhosted.org/packages/bd/8c/ee6cca8bdaf0f9a3704796022851a33cd37d1340bceaf4f6e991eb164e2e/ruff-0.11.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5", size = 10176850, upload-time = "2025-05-15T14:08:23.682Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ce/4e27e131a434321b3b7c66512c3ee7505b446eb1c8a80777c023f7e876e6/ruff-0.11.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641", size = 11759787, upload-time = "2025-05-15T14:08:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/58/de/1e2e77fc72adc7cf5b5123fd04a59ed329651d3eab9825674a9e640b100b/ruff-0.11.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947", size = 12430479, upload-time = "2025-05-15T14:08:28.013Z" }, - { url = "https://files.pythonhosted.org/packages/07/ed/af0f2340f33b70d50121628ef175523cc4c37619e98d98748c85764c8d88/ruff-0.11.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4", size = 11919760, upload-time = "2025-05-15T14:08:30.956Z" }, - { url = "https://files.pythonhosted.org/packages/24/09/d7b3d3226d535cb89234390f418d10e00a157b6c4a06dfbe723e9322cb7d/ruff-0.11.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f", size = 14041747, upload-time = "2025-05-15T14:08:33.297Z" }, - { url = "https://files.pythonhosted.org/packages/62/b3/a63b4e91850e3f47f78795e6630ee9266cb6963de8f0191600289c2bb8f4/ruff-0.11.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b", size = 11550657, upload-time = "2025-05-15T14:08:35.639Z" }, - { url = "https://files.pythonhosted.org/packages/46/63/a4f95c241d79402ccdbdb1d823d156c89fbb36ebfc4289dce092e6c0aa8f/ruff-0.11.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2", size = 10489671, upload-time = "2025-05-15T14:08:38.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9b/c2238bfebf1e473495659c523d50b1685258b6345d5ab0b418ca3f010cd7/ruff-0.11.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523", size = 10160135, upload-time = "2025-05-15T14:08:41.247Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ef/ba7251dd15206688dbfba7d413c0312e94df3b31b08f5d695580b755a899/ruff-0.11.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125", size = 11170179, upload-time = "2025-05-15T14:08:43.762Z" }, - { url = "https://files.pythonhosted.org/packages/73/9f/5c336717293203ba275dbfa2ea16e49b29a9fd9a0ea8b6febfc17e133577/ruff-0.11.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad", size = 11626021, upload-time = "2025-05-15T14:08:46.451Z" }, - { url = "https://files.pythonhosted.org/packages/d9/2b/162fa86d2639076667c9aa59196c020dc6d7023ac8f342416c2f5ec4bda0/ruff-0.11.10-py3-none-win32.whl", hash = "sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19", size = 10494958, upload-time = "2025-05-15T14:08:49.601Z" }, - { url = "https://files.pythonhosted.org/packages/24/f3/66643d8f32f50a4b0d09a4832b7d919145ee2b944d43e604fbd7c144d175/ruff-0.11.10-py3-none-win_amd64.whl", hash = "sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224", size = 11650285, upload-time = "2025-05-15T14:08:52.392Z" }, - { url = "https://files.pythonhosted.org/packages/95/3a/2e8704d19f376c799748ff9cb041225c1d59f3e7711bc5596c8cfdc24925/ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1", size = 10765278, upload-time = "2025-05-15T14:08:54.56Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" }, + { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" }, + { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" }, + { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" }, + { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" }, + { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" }, + { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" }, + { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" }, ] [[package]] @@ -4779,15 +4730,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.29.1" +version = "2.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/67/d552a5f8e5a6a56b2feea6529e2d8ccd54349084c84176d5a1f7295044bc/sentry_sdk-2.29.1.tar.gz", hash = "sha256:8d4a0206b95fa5fe85e5e7517ed662e3888374bdc342c00e435e10e6d831aa6d", size = 325518, upload-time = "2025-05-19T14:27:38.512Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/4c/af31e0201b48469786ddeb1bf6fd3dfa3a291cc613a0fe6a60163a7535f9/sentry_sdk-2.30.0.tar.gz", hash = "sha256:436369b02afef7430efb10300a344fb61a11fe6db41c2b11f41ee037d2dd7f45", size = 326767, upload-time = "2025-06-12T10:34:34.733Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/e5/da07b0bd832cefd52d16f2b9bbbe31624d57552602c06631686b93ccb1bd/sentry_sdk-2.29.1-py2.py3-none-any.whl", hash = "sha256:90862fe0616ded4572da6c9dadb363121a1ae49a49e21c418f0634e9d10b4c19", size = 341553, upload-time = "2025-05-19T14:27:36.882Z" }, + { url = "https://files.pythonhosted.org/packages/5a/99/31ac6faaae33ea698086692638f58d14f121162a8db0039e68e94135e7f1/sentry_sdk-2.30.0-py2.py3-none-any.whl", hash = "sha256:59391db1550662f746ea09b483806a631c3ae38d6340804a1a4c0605044f6877", size = 343149, upload-time = "2025-06-12T10:34:32.896Z" }, ] [[package]] @@ -4824,11 +4775,11 @@ wheels = [ [[package]] name = "setuptools" -version = "80.8.0" +version = "80.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/d2/ec1acaaff45caed5c2dedb33b67055ba9d4e96b091094df90762e60135fe/setuptools-80.8.0.tar.gz", hash = "sha256:49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257", size = 1319720, upload-time = "2025-05-20T14:02:53.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/29/93c53c098d301132196c3238c312825324740851d77a8500a2462c0fd888/setuptools-80.8.0-py3-none-any.whl", hash = "sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0", size = 1201470, upload-time = "2025-05-20T14:02:51.348Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] [[package]] @@ -4990,14 +4941,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.32.0.20250515" +version = "2.32.4.20250611" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/c1/cdc4f9b8cfd9130fbe6276db574f114541f4231fcc6fb29648289e6e3390/types_requests-2.32.0.20250515.tar.gz", hash = "sha256:09c8b63c11318cb2460813871aaa48b671002e59fda67ca909e9883777787581", size = 23012, upload-time = "2025-05-15T03:04:31.817Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/0f/68a997c73a129287785f418c1ebb6004f81e46b53b3caba88c0e03fcd04a/types_requests-2.32.0.20250515-py3-none-any.whl", hash = "sha256:f8eba93b3a892beee32643ff836993f15a785816acca21ea0ffa006f05ef0fb2", size = 20635, upload-time = "2025-05-15T03:04:30.5Z" }, + { 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" }, ] [[package]] @@ -5011,20 +4962,20 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.13.2" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, ] [[package]] name = "urllib3" -version = "2.4.0" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] @@ -5103,50 +5054,50 @@ wheels = [ [[package]] name = "yarl" -version = "1.20.0" +version = "1.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/51/c0edba5219027f6eab262e139f73e2417b0f4efffa23bf562f6e18f76ca5/yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", size = 185258, upload-time = "2025-04-17T00:45:14.661Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/82/a59d8e21b20ffc836775fa7daedac51d16bb8f3010c4fcb495c4496aa922/yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3", size = 145178, upload-time = "2025-04-17T00:42:04.511Z" }, - { url = "https://files.pythonhosted.org/packages/ba/81/315a3f6f95947cfbf37c92d6fbce42a1a6207b6c38e8c2b452499ec7d449/yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a", size = 96859, upload-time = "2025-04-17T00:42:06.43Z" }, - { url = "https://files.pythonhosted.org/packages/ad/17/9b64e575583158551b72272a1023cdbd65af54fe13421d856b2850a6ddb7/yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2", size = 94647, upload-time = "2025-04-17T00:42:07.976Z" }, - { url = "https://files.pythonhosted.org/packages/2c/29/8f291e7922a58a21349683f6120a85701aeefaa02e9f7c8a2dc24fe3f431/yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e", size = 355788, upload-time = "2025-04-17T00:42:09.902Z" }, - { url = "https://files.pythonhosted.org/packages/26/6d/b4892c80b805c42c228c6d11e03cafabf81662d371b0853e7f0f513837d5/yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9", size = 344613, upload-time = "2025-04-17T00:42:11.768Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0e/517aa28d3f848589bae9593717b063a544b86ba0a807d943c70f48fcf3bb/yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a", size = 370953, upload-time = "2025-04-17T00:42:13.983Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/5bd09d2f1ad6e6f7c2beae9e50db78edd2cca4d194d227b958955573e240/yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2", size = 369204, upload-time = "2025-04-17T00:42:16.386Z" }, - { url = "https://files.pythonhosted.org/packages/9c/85/d793a703cf4bd0d4cd04e4b13cc3d44149470f790230430331a0c1f52df5/yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2", size = 358108, upload-time = "2025-04-17T00:42:18.622Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/b6c71e13549c1f6048fbc14ce8d930ac5fb8bafe4f1a252e621a24f3f1f9/yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8", size = 346610, upload-time = "2025-04-17T00:42:20.9Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1a/d6087d58bdd0d8a2a37bbcdffac9d9721af6ebe50d85304d9f9b57dfd862/yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902", size = 365378, upload-time = "2025-04-17T00:42:22.926Z" }, - { url = "https://files.pythonhosted.org/packages/02/84/e25ddff4cbc001dbc4af76f8d41a3e23818212dd1f0a52044cbc60568872/yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791", size = 356919, upload-time = "2025-04-17T00:42:25.145Z" }, - { url = "https://files.pythonhosted.org/packages/04/76/898ae362353bf8f64636495d222c8014c8e5267df39b1a9fe1e1572fb7d0/yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f", size = 364248, upload-time = "2025-04-17T00:42:27.475Z" }, - { url = "https://files.pythonhosted.org/packages/1b/b0/9d9198d83a622f1c40fdbf7bd13b224a6979f2e1fc2cf50bfb1d8773c495/yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da", size = 378418, upload-time = "2025-04-17T00:42:29.333Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ce/1f50c1cc594cf5d3f5bf4a9b616fca68680deaec8ad349d928445ac52eb8/yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4", size = 383850, upload-time = "2025-04-17T00:42:31.668Z" }, - { url = "https://files.pythonhosted.org/packages/89/1e/a59253a87b35bfec1a25bb5801fb69943330b67cfd266278eb07e0609012/yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5", size = 381218, upload-time = "2025-04-17T00:42:33.523Z" }, - { url = "https://files.pythonhosted.org/packages/85/b0/26f87df2b3044b0ef1a7cf66d321102bdca091db64c5ae853fcb2171c031/yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6", size = 86606, upload-time = "2025-04-17T00:42:35.873Z" }, - { url = "https://files.pythonhosted.org/packages/33/46/ca335c2e1f90446a77640a45eeb1cd8f6934f2c6e4df7db0f0f36ef9f025/yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb", size = 93374, upload-time = "2025-04-17T00:42:37.586Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/3efdcb83073df978bb5b1a9cc0360ce596680e6c3fac01f2a994ccbb8939/yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", size = 147089, upload-time = "2025-04-17T00:42:39.602Z" }, - { url = "https://files.pythonhosted.org/packages/60/c3/9e776e98ea350f76f94dd80b408eaa54e5092643dbf65fd9babcffb60509/yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", size = 97706, upload-time = "2025-04-17T00:42:41.469Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/45cdfb64a3b855ce074ae607b9fc40bc82e7613b94e7612b030255c93a09/yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", size = 95719, upload-time = "2025-04-17T00:42:43.666Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4e/929633b249611eeed04e2f861a14ed001acca3ef9ec2a984a757b1515889/yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", size = 343972, upload-time = "2025-04-17T00:42:45.391Z" }, - { url = "https://files.pythonhosted.org/packages/49/fd/047535d326c913f1a90407a3baf7ff535b10098611eaef2c527e32e81ca1/yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", size = 339639, upload-time = "2025-04-17T00:42:47.552Z" }, - { url = "https://files.pythonhosted.org/packages/48/2f/11566f1176a78f4bafb0937c0072410b1b0d3640b297944a6a7a556e1d0b/yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", size = 353745, upload-time = "2025-04-17T00:42:49.406Z" }, - { url = "https://files.pythonhosted.org/packages/26/17/07dfcf034d6ae8837b33988be66045dd52f878dfb1c4e8f80a7343f677be/yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", size = 354178, upload-time = "2025-04-17T00:42:51.588Z" }, - { url = "https://files.pythonhosted.org/packages/15/45/212604d3142d84b4065d5f8cab6582ed3d78e4cc250568ef2a36fe1cf0a5/yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", size = 349219, upload-time = "2025-04-17T00:42:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e0/a10b30f294111c5f1c682461e9459935c17d467a760c21e1f7db400ff499/yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", size = 337266, upload-time = "2025-04-17T00:42:55.49Z" }, - { url = "https://files.pythonhosted.org/packages/33/a6/6efa1d85a675d25a46a167f9f3e80104cde317dfdf7f53f112ae6b16a60a/yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", size = 360873, upload-time = "2025-04-17T00:42:57.895Z" }, - { url = "https://files.pythonhosted.org/packages/77/67/c8ab718cb98dfa2ae9ba0f97bf3cbb7d45d37f13fe1fbad25ac92940954e/yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", size = 360524, upload-time = "2025-04-17T00:43:00.094Z" }, - { url = "https://files.pythonhosted.org/packages/bd/e8/c3f18660cea1bc73d9f8a2b3ef423def8dadbbae6c4afabdb920b73e0ead/yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", size = 365370, upload-time = "2025-04-17T00:43:02.242Z" }, - { url = "https://files.pythonhosted.org/packages/c9/99/33f3b97b065e62ff2d52817155a89cfa030a1a9b43fee7843ef560ad9603/yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", size = 373297, upload-time = "2025-04-17T00:43:04.189Z" }, - { url = "https://files.pythonhosted.org/packages/3d/89/7519e79e264a5f08653d2446b26d4724b01198a93a74d2e259291d538ab1/yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", size = 378771, upload-time = "2025-04-17T00:43:06.609Z" }, - { url = "https://files.pythonhosted.org/packages/3a/58/6c460bbb884abd2917c3eef6f663a4a873f8dc6f498561fc0ad92231c113/yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", size = 375000, upload-time = "2025-04-17T00:43:09.01Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/dd7ed1aa23fea996834278d7ff178f215b24324ee527df53d45e34d21d28/yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", size = 86355, upload-time = "2025-04-17T00:43:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c6/333fe0338305c0ac1c16d5aa7cc4841208d3252bbe62172e0051006b5445/yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", size = 92904, upload-time = "2025-04-17T00:43:13.087Z" }, - { url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124, upload-time = "2025-04-17T00:45:12.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833, upload-time = "2025-06-10T00:43:07.393Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070, upload-time = "2025-06-10T00:43:09.538Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818, upload-time = "2025-06-10T00:43:11.575Z" }, + { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003, upload-time = "2025-06-10T00:43:14.088Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537, upload-time = "2025-06-10T00:43:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358, upload-time = "2025-06-10T00:43:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362, upload-time = "2025-06-10T00:43:20.888Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979, upload-time = "2025-06-10T00:43:23.169Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274, upload-time = "2025-06-10T00:43:27.111Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294, upload-time = "2025-06-10T00:43:28.96Z" }, + { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169, upload-time = "2025-06-10T00:43:30.701Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776, upload-time = "2025-06-10T00:43:32.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341, upload-time = "2025-06-10T00:43:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988, upload-time = "2025-06-10T00:43:36.489Z" }, + { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113, upload-time = "2025-06-10T00:43:38.592Z" }, + { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485, upload-time = "2025-06-10T00:43:41.038Z" }, + { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686, upload-time = "2025-06-10T00:43:42.692Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, ] [[package]]