diff --git a/.github/workflows/build-all-tinygrad-models.yaml b/.github/workflows/build-all-tinygrad-models.yaml index 00864d38a0..e901baee0c 100644 --- a/.github/workflows/build-all-tinygrad-models.yaml +++ b/.github/workflows/build-all-tinygrad-models.yaml @@ -140,7 +140,7 @@ jobs: run: | echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json built=(); while IFS= read -r line; do built+=("$line"); done < <( - ls output | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}' + find output -maxdepth 1 -name 'model-*' -printf "%f\n" | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}' ) jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \ 'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' matrix.json > retry_matrix.json @@ -168,6 +168,7 @@ jobs: if: ${{ !cancelled() && (needs.get_and_build.result != 'failure' || needs.retry_get_and_build.result == 'success' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '')) }} runs-on: ubuntu-latest strategy: + fail-fast: false max-parallel: 1 matrix: model: ${{ fromJson(needs.setup.outputs.model_matrix) }} diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 3c652b6ce6..9be42cd041 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -43,7 +43,6 @@ jobs: with: submodules: true - name: uv lock - if: github.repository == 'commaai/openpilot' run: | python3 -m ensurepip --upgrade pip3 install uv @@ -57,6 +56,9 @@ jobs: - name: bump submodules run: | git config --global --add safe.directory '*' + git config submodule.msgq.update none + git config submodule.rednose_repo.update none + git config submodule.teleoprtc_repo.update none git config submodule.tinygrad.update none git submodule update --remote git add . diff --git a/.gitmodules b/.gitmodules index cd6cf2168f..5c5d72a7dc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,7 +15,7 @@ url = https://github.com/commaai/teleoprtc [submodule "tinygrad"] path = tinygrad_repo - url = https://github.com/commaai/tinygrad.git + url = https://github.com/sunnypilot/tinygrad.git [submodule "sunnypilot/neural_network_data"] path = sunnypilot/neural_network_data url = https://github.com/sunnypilot/neural-network-data.git diff --git a/Dockerfile.sunnypilot b/Dockerfile.sunnypilot index 88a226ad08..abc207171e 100644 --- a/Dockerfile.sunnypilot +++ b/Dockerfile.sunnypilot @@ -9,4 +9,6 @@ WORKDIR ${OPENPILOT_PATH} COPY . ${OPENPILOT_PATH}/ -RUN scons --cache-readonly -j$(nproc) +ENV UV_BIN="/home/batman/.local/bin/" +ENV PATH="$UV_BIN:$PATH" +RUN UV_PROJECT_ENVIRONMENT=$VIRTUAL_ENV uv run scons --cache-readonly -j$(nproc) diff --git a/common/params_keys.h b/common/params_keys.h index f2a63ec1b1..dba0742268 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -137,6 +137,7 @@ inline static std::unordered_map keys = { {"ApiCache_DriveStats", {PERSISTENT, JSON}}, {"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}}, {"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}}, + {"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds {"BlinkerMinLateralControlSpeed", {PERSISTENT | BACKUP, INT, "20"}}, // MPH or km/h {"BlinkerPauseLateralControl", {PERSISTENT | BACKUP, INT, "0"}}, {"Brightness", {PERSISTENT | BACKUP, INT, "0"}}, diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 903cb5d389..7865e86f66 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -50,7 +50,7 @@ def tg_compile(flags, model_name): # Compile small models for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: flags = { - 'larch64': 'DEV=QCOM', + 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0', 'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env }.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0') tg_compile(flags, model_name) diff --git a/selfdrive/ui/mici/onroad/model_renderer.py b/selfdrive/ui/mici/onroad/model_renderer.py index 0908de0bf4..ca051c6d94 100644 --- a/selfdrive/ui/mici/onroad/model_renderer.py +++ b/selfdrive/ui/mici/onroad/model_renderer.py @@ -12,7 +12,7 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient from openpilot.system.ui.widgets import Widget -from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP +from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP, ModelRendererSP CLIP_MARGIN = 500 MIN_DRAW_DISTANCE = 10.0 @@ -51,9 +51,10 @@ class LeadVehicle: fill_alpha: int = 0 -class ModelRenderer(Widget): +class ModelRenderer(Widget, ModelRendererSP): def __init__(self): - super().__init__() + Widget.__init__(self) + ModelRendererSP.__init__(self) self._longitudinal_control = False self._experimental_mode = False self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps) @@ -340,6 +341,10 @@ class ModelRenderer(Widget): allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control self._blend_filter.update(int(allow_throttle)) + if ui_state.rainbow_path: + self.rainbow_path.draw_rainbow_path(self._rect, self._path) + return + if self._experimental_mode: # Draw with acceleration coloring if ui_state.status == UIStatus.DISENGAGED: diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py index 9439c588d3..671174ac7a 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py @@ -4,27 +4,190 @@ 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.system.ui.widgets.scroller_tici import Scroller +from enum import IntEnum + +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_settings import SpeedLimitSettingsLayout +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.multilang import tr, tr_noop +from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp, simple_button_item_sp from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.scroller_tici import Scroller + + +class PanelType(IntEnum): + CRUISE = 0 + SLA = 1 + + +ICBM_DESC = tr_noop("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons " + + "by emulating button presses for limited longitudinal control.") +ICMB_UNAVAILABLE = tr_noop("Intelligent Cruise Button Management is currently unavailable on this platform.") +ICMB_UNAVAILABLE_LONG_AVAILABLE = tr_noop("Disable the sunnypilot Longitudinal Control (alpha) toggle to allow Intelligent Cruise Button Management.") +ICMB_UNAVAILABLE_LONG_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control is the default longitudinal control for this platform.") + +ACC_ENABLED_DESCRIPTION = tr_noop("Enable custom Short & Long press increments for cruise speed increase/decrease.") +ACC_NOLONG_DESCRIPTION = tr_noop("This feature can only be used with sunnypilot longitudinal control enabled.") +ACC_PCMCRUISE_DISABLED_DESCRIPTION = tr_noop("This feature is not supported on this platform due to vehicle limitations.") +ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.") class CruiseLayout(Widget): def __init__(self): super().__init__() + self._current_panel = PanelType.CRUISE + self._speed_limit_layout = SpeedLimitSettingsLayout(lambda: self._set_current_panel(PanelType.CRUISE)) - self._params = Params() items = self._initialize_items() self._scroller = Scroller(items, line_separator=True, spacing=0) def _initialize_items(self): + self.icbm_toggle = toggle_item_sp( + title=tr("Intelligent Cruise Button Management (ICBM) (Alpha)"), + description="", + param="IntelligentCruiseButtonManagement") + + self.scc_v_toggle = toggle_item_sp( + title=tr("Smart Cruise Control - Vision"), + description=tr("Use vision path predictions to estimate the appropriate speed to drive through turns ahead."), + param="SmartCruiseControlVision") + + self.scc_m_toggle = toggle_item_sp( + title=tr("Smart Cruise Control - Map"), + description=tr("Use map data to estimate the appropriate speed to drive through turns ahead."), + param="SmartCruiseControlMap") + + self.custom_acc_toggle = toggle_item_sp( + title=tr("Custom ACC Speed Increments"), + description="", + param="CustomAccIncrementsEnabled", + callback=self._on_custom_acc_toggle) + + self.custom_acc_short_increment = option_item_sp( + title=tr("Short Press Increment"), + param="CustomAccShortPressIncrement", + min_value=1, max_value=10, value_change_step=1, + inline=True) + + self.custom_acc_long_increment = option_item_sp( + title=tr("Long Press Increment"), + param="CustomAccLongPressIncrement", + value_map={1: 1, 2: 5, 3: 10}, + min_value=1, max_value=3, value_change_step=1, + inline=True) + + self.sla_settings_button = simple_button_item_sp( + button_text=lambda: tr("Speed Limit"), + button_width=800, + callback=lambda: self._set_current_panel(PanelType.SLA) + ) + + self.dec_toggle = toggle_item_sp( + title=tr("Enable Dynamic Experimental Control"), + description=tr("Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal."), + param="DynamicExperimentalControl") + items = [ + self.icbm_toggle, + self.dec_toggle, + self.scc_v_toggle, + self.scc_m_toggle, + self.custom_acc_toggle, + self.custom_acc_short_increment, + self.custom_acc_long_increment, + self.sla_settings_button, ] return items def _render(self, rect): - self._scroller.render(rect) + if self._current_panel == PanelType.SLA: + self._speed_limit_layout.render(rect) + else: + self._scroller.render(rect) def show_event(self): + self._set_current_panel(PanelType.CRUISE) self._scroller.show_event() + self.icbm_toggle.show_description(True) + self.custom_acc_toggle.show_description(True) + + def _set_current_panel(self, panel: PanelType): + self._current_panel = panel + if panel == PanelType.SLA: + self._speed_limit_layout.show_event() + + def _update_state(self): + super()._update_state() + + if ui_state.CP is not None and ui_state.CP_SP is not None: + has_icbm = ui_state.has_icbm + has_long = ui_state.has_longitudinal_control + + if ui_state.CP_SP.intelligentCruiseButtonManagementAvailable and not has_long: + self.icbm_toggle.action_item.set_enabled(ui_state.is_offroad()) + self.icbm_toggle.set_description(tr(ICBM_DESC)) + else: + ui_state.params.remove("IntelligentCruiseButtonManagement") + self.icbm_toggle.action_item.set_enabled(False) + + long_desc = ICMB_UNAVAILABLE + if has_long: + if ui_state.CP.alphaLongitudinalAvailable: + long_desc += " " + ICMB_UNAVAILABLE_LONG_AVAILABLE + else: + long_desc += " " + ICMB_UNAVAILABLE_LONG_UNAVAILABLE + + new_desc = "" + tr(long_desc) + "\n\n" + tr(ICBM_DESC) + if self.icbm_toggle.description != new_desc: + self.icbm_toggle.set_description(new_desc) + self.icbm_toggle.show_description(True) + + if has_long or has_icbm: + self.custom_acc_toggle.action_item.set_enabled(((has_long and not ui_state.CP.pcmCruise) or has_icbm) and ui_state.is_offroad()) + self.dec_toggle.action_item.set_enabled(has_long) + self.scc_v_toggle.action_item.set_enabled(True) + self.scc_m_toggle.action_item.set_enabled(True) + else: + ui_state.params.remove("CustomAccIncrementsEnabled") + ui_state.params.remove("DynamicExperimentalControl") + ui_state.params.remove("SmartCruiseControlVision") + ui_state.params.remove("SmartCruiseControlMap") + self.custom_acc_toggle.action_item.set_enabled(False) + self.dec_toggle.action_item.set_enabled(False) + self.scc_v_toggle.action_item.set_enabled(False) + self.scc_m_toggle.action_item.set_enabled(False) + + else: + has_icbm = has_long = False + self.icbm_toggle.action_item.set_enabled(False) + self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION)) + + show_custom_acc_desc = False + + if ui_state.is_offroad(): + new_custom_acc_desc = tr(ONROAD_ONLY_DESCRIPTION) + show_custom_acc_desc = True + else: + if has_long or has_icbm: + if has_long and ui_state.CP.pcmCruise: + new_custom_acc_desc = tr(ACC_PCMCRUISE_DISABLED_DESCRIPTION) + show_custom_acc_desc = True + else: + new_custom_acc_desc = tr(ACC_ENABLED_DESCRIPTION) + else: + new_custom_acc_desc = tr(ACC_NOLONG_DESCRIPTION) + show_custom_acc_desc = True + self.custom_acc_toggle.action_item.set_state(False) + + if self.custom_acc_toggle.description != new_custom_acc_desc: + self.custom_acc_toggle.set_description(new_custom_acc_desc) + if show_custom_acc_desc: + self.custom_acc_toggle.show_description(True) + + self._on_custom_acc_toggle(self.custom_acc_toggle.action_item.get_state()) + + def _on_custom_acc_toggle(self, state): + self.custom_acc_short_increment.set_visible(state) + self.custom_acc_long_increment.set_visible(state) + self.custom_acc_short_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled) + self.custom_acc_long_increment.action_item.set_enabled(self.custom_acc_toggle.action_item.enabled) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py new file mode 100644 index 0000000000..e6846744a2 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_policy.py @@ -0,0 +1,65 @@ +""" +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 collections.abc import Callable + +import pyray as rl +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp +from openpilot.system.ui.widgets.network import NavButton +from openpilot.system.ui.widgets.scroller_tici import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description + +SPEED_LIMIT_POLICY_BUTTONS = [tr("Car Only"), tr("Map Only"), tr("Car First"), tr("Map First"), tr("Combined")] + +SPEED_LIMIT_POLICY_DESCRIPTIONS = [ + tr("Car Only: Use Speed Limit data only from Car"), + tr("Map Only: Use Speed Limit data only from OpenStreetMaps"), + tr("Car First: Use Speed Limit data from Car if available, else use from OpenStreetMaps"), + tr("Map First: Use Speed Limit data from OpenStreetMaps if available, else use from Car"), + tr("Combined: Use combined Speed Limit data from Car & OpenStreetMaps") +] + + +class SpeedLimitPolicyLayout(Widget): + def __init__(self, back_btn_callback: Callable): + super().__init__() + self._back_button = NavButton(tr("Back")) + self._back_button.set_click_callback(back_btn_callback) + + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=False, spacing=0) + + def _initialize_items(self): + self._speed_limit_policy = multiple_button_item_sp( + title=lambda: tr("Speed Limit Source"), + description=self._get_policy_description, + buttons=SPEED_LIMIT_POLICY_BUTTONS, + param="SpeedLimitPolicy", + button_width=250, + ) + + items = [ + self._speed_limit_policy + ] + return items + + @staticmethod + def _get_policy_description(): + return get_highlighted_description(ui_state.params, "SpeedLimitPolicy", SPEED_LIMIT_POLICY_DESCRIPTIONS) + + def _render(self, rect): + self._back_button.set_position(self._rect.x, self._rect.y + 20) + self._back_button.render() + + content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40) + self._scroller.render(content_rect) + + def show_event(self): + self._scroller.show_event() + self._speed_limit_policy.show_description(True) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py new file mode 100644 index 0000000000..c14330d9ac --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise_sub_layouts/speed_limit_settings.py @@ -0,0 +1,178 @@ +""" +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 collections.abc import Callable +from enum import IntEnum + +import pyray as rl +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise_sub_layouts.speed_limit_policy import SpeedLimitPolicyLayout +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import OffsetType as SpeedLimitOffsetType +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.sunnypilot.widgets import get_highlighted_description +from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, option_item_sp, simple_button_item_sp, LineSeparatorSP +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.network import NavButton +from openpilot.system.ui.widgets.scroller_tici import Scroller + +SPEED_LIMIT_MODE_BUTTONS = [tr("Off"), tr("Info"), tr("Warning"), tr("Assist")] +SPEED_LIMIT_OFFSET_TYPE_BUTTONS = [tr("None"), tr("Fixed"), tr("%")] + +SPEED_LIMIT_MODE_DESCRIPTIONS = [ + tr("Off: Disables the Speed Limit functions."), + tr("Information: Displays the current road's speed limit."), + tr("Warning: Provides a warning when exceeding the current road's speed limit."), + tr("Assist: Adjusts the vehicle's cruise speed based on the current road's speed limit when operating the +/- buttons."), +] + +SPEED_LIMIT_OFFSET_DESCRIPTIONS = [ + tr("None: No Offset"), + tr("Fixed: Adds a fixed offset [Speed Limit + Offset]"), + tr("Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)]"), +] + + +class PanelType(IntEnum): + SETTINGS = 0 + POLICY = 1 + + +class SpeedLimitSettingsLayout(Widget): + def __init__(self, back_btn_callback: Callable): + super().__init__() + self._current_panel = PanelType.SETTINGS + + self._back_button = NavButton(tr("Back")) + self._back_button.set_click_callback(back_btn_callback) + + self._policy_layout = SpeedLimitPolicyLayout(lambda: self._set_current_panel(PanelType.SETTINGS)) + + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=False, spacing=0) + + def _initialize_items(self): + self._speed_limit_mode = multiple_button_item_sp( + title=lambda: tr("Speed Limit"), + description=self._get_mode_description, + buttons=SPEED_LIMIT_MODE_BUTTONS, + param="SpeedLimitMode", + button_width=380, + ) + + self._source_button = simple_button_item_sp( + button_text=lambda: tr("Customize Source"), + button_width=720, + callback=lambda: self._set_current_panel(PanelType.POLICY) + ) + + self._speed_limit_offset_type = multiple_button_item_sp( + title=lambda: tr("Speed Limit Offset"), + description="", + buttons=SPEED_LIMIT_OFFSET_TYPE_BUTTONS, + param="SpeedLimitOffsetType", + button_width=450, + ) + + self._speed_limit_value_offset = option_item_sp( + title="", + param="SpeedLimitValueOffset", + min_value=-30, + max_value=30, + description=self._get_offset_description, + label_callback=self._get_offset_label, + ) + + items = [ + self._speed_limit_mode, + LineSeparatorSP(40), + self._source_button, + LineSeparatorSP(40), + self._speed_limit_offset_type, + self._speed_limit_value_offset + ] + return items + + def _set_current_panel(self, panel: PanelType): + self._current_panel = panel + if panel == PanelType.POLICY: + self._policy_layout.show_event() + + @staticmethod + def _get_mode_description(): + return get_highlighted_description(ui_state.params, "SpeedLimitMode", SPEED_LIMIT_MODE_DESCRIPTIONS) + + @staticmethod + def _get_offset_description(): + return get_highlighted_description(ui_state.params, "SpeedLimitOffsetType", SPEED_LIMIT_OFFSET_DESCRIPTIONS) + + @staticmethod + def _get_offset_label(value): + offset_type = int(ui_state.params.get("SpeedLimitOffsetType", return_default=True)) + unit = tr("km/h") if ui_state.is_metric else tr("mph") + + if offset_type == int(SpeedLimitOffsetType.percentage): + return f"{value}%" + elif offset_type == int(SpeedLimitOffsetType.fixed): + return f"{value} {unit}" + return str(value) + + def _update_state(self): + super()._update_state() + + speed_limit_mode_param = ui_state.params.get("SpeedLimitMode", return_default=True) + if ui_state.CP is not None and ui_state.CP_SP is not None: + brand = ui_state.CP.brand + has_long = ui_state.has_longitudinal_control + has_icbm = ui_state.has_icbm + + """ + Speed Limit Assist is available when: + - has_long or has_icbm, and + - is not a release branch or not a disallowed brand, and + - is not always disallwed + """ + sla_disallow_in_release = brand == "tesla" and ui_state.is_sp_release + sla_always_disallow = brand == "rivian" + sla_available = (has_long or has_icbm) and not sla_disallow_in_release and not sla_always_disallow + + if not sla_available and speed_limit_mode_param == int(SpeedLimitMode.assist): + ui_state.params.put("SpeedLimitMode", int(SpeedLimitMode.warning)) + + else: + sla_available = False + + if not sla_available: + self._speed_limit_mode.action_item.set_enabled_buttons({ + int(SpeedLimitMode.off), + int(SpeedLimitMode.information), + int(SpeedLimitMode.warning), + }) + else: + self._speed_limit_mode.action_item.set_enabled_buttons(None) + + offset_type = ui_state.params.get("SpeedLimitOffsetType", return_default=True) + self._speed_limit_value_offset.set_visible(offset_type != int(SpeedLimitOffsetType.off)) + + def _render(self, rect): + if self._current_panel == PanelType.POLICY: + self._policy_layout.render(rect) + return + + self._back_button.set_position(self._rect.x, self._rect.y + 20) + self._back_button.render() + + content_rect = rl.Rectangle(rect.x, rect.y + self._back_button.rect.height + 40, rect.width, rect.height - self._back_button.rect.height - 40) + self._scroller.render(content_rect) + + def show_event(self): + self._current_panel = PanelType.SETTINGS + self._scroller.show_event() + self._speed_limit_mode.show_description(True) + + def hide_event(self): + self._current_panel = PanelType.SETTINGS + self._scroller.hide_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering.py b/selfdrive/ui/sunnypilot/layouts/settings/steering.py index f82c4097c3..14d840138e 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering.py @@ -72,6 +72,15 @@ class SteeringLayout(Widget): description="", label_callback=lambda speed: f'{speed} {"km/h" if ui_state.is_metric else "mph"}', ) + self._blinker_reengage_delay = option_item_sp( + param="BlinkerLateralReengageDelay", + title=lambda: tr("Post-Blinker Delay"), + min_value=0, + max_value=10, + value_change_step=1, + description=lambda: tr("Delay before lateral control resumes after the turn signal ends."), + label_callback=lambda delay: f'{delay} {"s"}' + ) self._torque_control_toggle = toggle_item_sp( param="EnforceTorqueControl", title=lambda: tr("Enforce Torque Lateral Control"), @@ -96,6 +105,7 @@ class SteeringLayout(Widget): LineSeparatorSP(40), self._blinker_control_toggle, self._blinker_control_options, + self._blinker_reengage_delay, LineSeparatorSP(40), self._torque_control_toggle, self._torque_customization_button, @@ -128,6 +138,7 @@ class SteeringLayout(Widget): self._mads_toggle.action_item.set_enabled(ui_state.is_offroad()) self._mads_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._mads_toggle.action_item.get_state()) self._blinker_control_options.set_visible(self._blinker_control_toggle.action_item.get_state()) + self._blinker_reengage_delay.set_visible(self._blinker_control_toggle.action_item.get_state()) enforce_torque_enabled = self._torque_control_toggle.action_item.get_state() nnlc_enabled = self._nnlc_toggle.action_item.get_state() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py index d3f38e04d1..3542adf285 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/mads_settings.py @@ -9,6 +9,7 @@ import pyray as rl from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.network import NavButton @@ -112,7 +113,7 @@ class MadsSettingsLayout(Widget): if self._mads_limited_settings(): ui_state.params.remove("MadsMainCruiseAllowed") ui_state.params.put_bool("MadsUnifiedEngagementMode", True) - ui_state.params.put("MadsSteeringMode", 2) + ui_state.params.put("MadsSteeringMode", MadsSteeringModeOnBrake.DISENGAGE) self._main_cruise_toggle.action_item.set_enabled(False) self._main_cruise_toggle.action_item.set_state(False) @@ -122,9 +123,9 @@ class MadsSettingsLayout(Widget): self._unified_engagement_toggle.action_item.set_state(True) self._unified_engagement_toggle.set_description("" + DEFAULT_TO_ON + "
" + MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC) - self._steering_mode.action_item.set_enabled(False) self._steering_mode.set_description(STATUS_DISENGAGE_ONLY) - self._steering_mode.action_item.set_selected_button(2) + self._steering_mode.action_item.set_selected_button(MadsSteeringModeOnBrake.DISENGAGE) + self._steering_mode.action_item.set_enabled_buttons({MadsSteeringModeOnBrake.DISENGAGE}) else: self._main_cruise_toggle.action_item.set_enabled(True) self._main_cruise_toggle.set_description(MADS_MAIN_CRUISE_BASE_DESC) @@ -133,3 +134,4 @@ class MadsSettingsLayout(Widget): self._unified_engagement_toggle.set_description(MADS_UNIFIED_ENGAGEMENT_MODE_BASE_DESC) self._steering_mode.action_item.set_enabled(True) + self._steering_mode.action_item.set_enabled_buttons(None) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py index 00baf0cccf..7955f13202 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py @@ -355,5 +355,10 @@ class SunnylinkLayout(Widget): def show_event(self): super().show_event() + ui_state.sunnylink_state.set_settings_open(True) self._scroller.show_event() self._sunnylink_description.set_visible(False) + + def hide_event(self): + super().hide_event() + ui_state.sunnylink_state.set_settings_open(False) diff --git a/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py b/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py index 5a718947cf..e3de9401ce 100644 --- a/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py +++ b/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py @@ -6,8 +6,14 @@ See the LICENSE.md file in the root directory for more details. """ import pyray as rl from openpilot.selfdrive.ui.ui_state import UIStatus +from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath LANE_LINE_COLORS_SP = { UIStatus.LAT_ONLY: rl.Color(0, 255, 64, 255), UIStatus.LONG_ONLY: rl.Color(0, 255, 64, 255), } + + +class ModelRendererSP: + def __init__(self): + self.rainbow_path = RainbowPath() diff --git a/selfdrive/ui/sunnypilot/onroad/turn_signal.py b/selfdrive/ui/sunnypilot/onroad/turn_signal.py index 04fff7db76..bd1aa7ee10 100644 --- a/selfdrive/ui/sunnypilot/onroad/turn_signal.py +++ b/selfdrive/ui/sunnypilot/onroad/turn_signal.py @@ -72,72 +72,28 @@ class TurnSignalWidget(Widget): class TurnSignalController: - def __init__(self, config: TurnSignalConfig | None = None): - self._config = config or TurnSignalConfig() + def __init__(self): + self._config = TurnSignalConfig() self._left_signal = TurnSignalWidget(direction=IconSide.left) self._right_signal = TurnSignalWidget(direction=IconSide.right) - self._last_icon_side = None + + @staticmethod + def _update_signal(signal, blindspot, blinker): + if ui_state.blindspot and blindspot: + signal.activate('blind_spot') + elif ui_state.turn_signals and blinker: + signal.activate('signal') + else: + signal.deactivate() def update(self): - sm = ui_state.sm - ss = sm['selfdriveState'] + CS = ui_state.sm['carState'] - event_name = ss.alertType.split('/')[0] if ss.alertType else '' - - if event_name == 'preLaneChangeLeft': - self._last_icon_side = IconSide.left - self._left_signal.activate('signal') - self._right_signal.deactivate() - - elif event_name == 'preLaneChangeRight': - self._last_icon_side = IconSide.right - self._right_signal.activate('signal') - self._left_signal.deactivate() - - elif event_name == 'laneChange': - if self._last_icon_side == IconSide.left: - self._left_signal.activate('signal') - self._right_signal.deactivate() - elif self._last_icon_side == IconSide.right: - self._right_signal.activate('signal') - self._left_signal.deactivate() - - elif event_name == 'laneChangeBlocked': - CS = sm['carState'] - if CS.leftBlinker: - icon_side = IconSide.left - elif CS.rightBlinker: - icon_side = IconSide.right - else: - icon_side = self._last_icon_side - - if icon_side == IconSide.left: - self._left_signal.activate('blind_spot') - self._right_signal.deactivate() - elif icon_side == IconSide.right: - self._right_signal.activate('blind_spot') - self._left_signal.deactivate() - - else: - self._last_icon_side = None - CS = sm['carState'] - - if CS.leftBlindspot: - self._left_signal.activate('blind_spot') - elif CS.leftBlinker: - self._left_signal.activate('signal') - else: - self._left_signal.deactivate() - - if CS.rightBlindspot: - self._right_signal.activate('blind_spot') - elif CS.rightBlinker: - self._right_signal.activate('signal') - else: - self._right_signal.deactivate() + self._update_signal(self._left_signal, CS.leftBlindspot, CS.leftBlinker) + self._update_signal(self._right_signal, CS.rightBlindspot, CS.rightBlinker) def render(self, rect: rl.Rectangle): - if not ui_state.turn_signals: + if not ui_state.turn_signals and not ui_state.blindspot: return x = rect.x + rect.width / 2 diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 3699098121..4d26e138b1 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -39,6 +39,9 @@ class UIStateSP: self.onroad_brightness_timer: int = 0 self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True) self.reset_onroad_sleep_timer() + self.CP_SP: custom.CarParamsSP | None = None + self.has_icbm: bool = False + self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch") def update(self) -> None: if self.sunnylink_enabled: @@ -121,6 +124,7 @@ class UIStateSP: CP_SP_bytes = self.params.get("CarParamsSPPersistent") if CP_SP_bytes is not None: self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP) + self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement") self.active_bundle = self.params.get("ModelManager_ActiveBundle") self.blindspot = self.params.get_bool("BlindSpot") self.chevron_metrics = self.params.get("ChevronInfo") @@ -138,15 +142,14 @@ class UIStateSP: self.torque_bar = self.params.get_bool("TorqueBar") self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI") self.turn_signals = self.params.get_bool("ShowTurnSignals") + self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True) class DeviceSP: - def __init__(self): - self._params = Params() - - def _set_awake(self, on: bool): - if on and self._params.get("DeviceBootMode", return_default=True) == 1: - self._params.put_bool("OffroadMode", True) + @staticmethod + def _set_awake(on: bool, _ui_state): + if _ui_state.boot_offroad_mode == 1 and not on: + _ui_state.params.put_bool("OffroadMode", True) @staticmethod def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float: diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index a9127ac3c5..f7a5d44d9a 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -299,7 +299,7 @@ class Device(DeviceSP): def _set_awake(self, on: bool): if on != self._awake: - DeviceSP._set_awake(self, on) + DeviceSP._set_awake(on, ui_state) self._awake = on cloudlog.debug(f"setting display power {int(on)}") HARDWARE.set_display_power(on) diff --git a/sunnypilot/modeld_v2/SConscript b/sunnypilot/modeld_v2/SConscript index 94033846b0..28d39a75f1 100644 --- a/sunnypilot/modeld_v2/SConscript +++ b/sunnypilot/modeld_v2/SConscript @@ -60,7 +60,7 @@ def tg_compile(flags, model_name): for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']: if File(f"models/{model_name}.onnx").exists(): flags = { - 'larch64': 'DEV=QCOM', + 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 IMAGE=2 JIT_BATCH_SIZE=0', 'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env }.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0') tg_compile(flags, model_name) diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index be0724db0d..e25078f52f 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -1,4 +1,11 @@ #!/usr/bin/env python3 +import os +from openpilot.system.hardware import TICI +os.environ['DEV'] = 'QCOM' if TICI else 'CPU' +USBGPU = "USBGPU" in os.environ +if USBGPU: + os.environ['DEV'] = 'AMD' + os.environ['AMD_IFACE'] = 'USB' import time import numpy as np import cereal.messaging as messaging diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 1d8da083a9..0de7496578 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -116,7 +116,7 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v11.json" + MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v14.json" def __init__(self, params: Params): self.params = params diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index ce6625a1f0..7fcf7f85ef 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -19,8 +19,8 @@ from openpilot.system.hardware.hw import Paths from pathlib import Path # see the README.md for more details on the model selector versioning -CURRENT_SELECTOR_VERSION = 13 -REQUIRED_MIN_SELECTOR_VERSION = 12 +CURRENT_SELECTOR_VERSION = 14 +REQUIRED_MIN_SELECTOR_VERSION = 14 USE_ONNX = os.getenv('USE_ONNX', PC) diff --git a/sunnypilot/models/runners/model_runner.py b/sunnypilot/models/runners/model_runner.py index 6902ed09b8..a49ff4d206 100644 --- a/sunnypilot/models/runners/model_runner.py +++ b/sunnypilot/models/runners/model_runner.py @@ -1,9 +1,7 @@ -import os from abc import abstractmethod, ABC import numpy as np from openpilot.sunnypilot.models.helpers import get_active_bundle -from openpilot.system.hardware import TICI from openpilot.sunnypilot.models.runners.constants import NumpyDict, ShapeDict, CLMemDict, FrameDict, Model, SliceDict, SEND_RAW_PRED from openpilot.system.hardware.hw import Paths import pickle @@ -11,15 +9,6 @@ import pickle CUSTOM_MODEL_PATH = Paths.model_root() -# Set QCOM environment variable for TICI devices, potentially enabling hardware acceleration -USBGPU = "USBGPU" in os.environ -if USBGPU: - os.environ['DEV'] = 'AMD' - os.environ['AMD_IFACE'] = 'USB' -else: - os.environ['DEV'] = 'QCOM' if TICI else 'CPU' - - class ModelData: """ Stores metadata and configuration for a specific machine learning model. diff --git a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py index 7b48e3086b..2df1c65e08 100644 --- a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py +++ b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py @@ -51,7 +51,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny self.input_to_dtype = {} self.input_to_device = {} for idx, name in enumerate(self.model_run.captured.expected_names): - info = self.model_run.captured.expected_st_vars_dtype_device[idx] + info = self.model_run.captured.expected_input_info[idx] self.input_to_dtype[name] = info[2] # dtype self.input_to_device[name] = info[3] # device @@ -84,7 +84,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny def _run_model(self) -> NumpyDict: """Runs the Tinygrad model inference and parses the outputs.""" - outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy() + outputs = self.model_run(**self.inputs).numpy().flatten() return self._parse_outputs(outputs) def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict: diff --git a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py index 2f27e7e42f..98757cd532 100644 --- a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py @@ -17,13 +17,16 @@ class BlinkerPauseLateral: self.enabled = self.params.get_bool("BlinkerPauseLateralControl") self.is_metric = self.params.get_bool("IsMetric") self.min_speed = 0 + self.reengage_delay = 0 + self.blinker_off_timer = 0.0 def get_params(self) -> None: self.enabled = self.params.get_bool("BlinkerPauseLateralControl") self.is_metric = self.params.get_bool("IsMetric") - self.min_speed = self.params.get("BlinkerMinLateralControlSpeed") + self.min_speed = self.params.get("BlinkerMinLateralControlSpeed", return_default=True) + self.reengage_delay = self.params.get("BlinkerLateralReengageDelay", return_default=True) - def update(self, CS: car.CarState) -> bool: + def update(self, CS: car.CarState, DT_CTRL: float = 0.01) -> bool: if not self.enabled: return False @@ -31,4 +34,11 @@ class BlinkerPauseLateral: 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) + below_speed = CS.vEgo < min_speed_ms + + if one_blinker and below_speed: + self.blinker_off_timer = self.reengage_delay + elif self.blinker_off_timer > 0: + self.blinker_off_timer -= DT_CTRL + + return bool((one_blinker and below_speed) or self.blinker_off_timer > 0) diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py index 9f6efffb55..d35b79a73b 100644 --- a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py +++ b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py @@ -5,6 +5,7 @@ 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 +import pytest import cereal.messaging as messaging from cereal import custom, log @@ -12,11 +13,52 @@ from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.selfdrive.modeld.constants import ModelConstants -from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision +from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control import MIN_V +from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision, _ENTERING_PRED_LAT_ACC_TH VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState +def _th_above_f32(th: float) -> float: + """ + Return the next representable float32 *above* `th`. + This avoids flaky comparisons around thresholds due to float32 rounding. + """ + th32 = np.float32(th) + above32 = np.nextafter(th32, np.float32(np.inf), dtype=np.float32) + return float(above32) + + +def _build_single_spike_filtered(n: int, base: float = 1.0) -> np.ndarray: + """ + Create an array where max() is >= threshold but p97 is < threshold. + This demonstrates the behavior difference vs np.amax(). + + Note: We intentionally construct using float32-representable values to match + the data path through cereal/capnp. + """ + th = float(_ENTERING_PRED_LAT_ACC_TH) + th32 = float(np.float32(th)) + + # numpy percentile default is linear interpolation: idx=(n-1)*p/100 + idx = (n - 1) * 0.97 + w = float(idx - np.floor(idx)) + + base32 = float(np.float32(base)) + + # Choose spike so that p97 = base + w*(spike-base) < th + # -> spike < base + (th-base)/w. Use a margin (0.9) and ensure spike >= th. + if w == 0.0: + spike = th32 + 1.0 + else: + spike = base32 + (th32 - base32) / w * 0.9 + spike = max(spike, th32 + 0.01) + + arr = np.full(n, base32, dtype=np.float32) + arr[-1] = np.float32(spike) + return arr + + def generate_modelV2(): model = messaging.new_message('modelV2') position = log.XYZTData.new_message() @@ -101,4 +143,72 @@ class TestSmartCruiseControlVision: self.scc_v.update(self.sm, True, False, 0., 0., 0.) assert self.scc_v.state == VisionState.enabled + @pytest.mark.parametrize( + "case, should_enter", + [ + ("p97_just_above_threshold", True), + ("single_spike_filtered", False), + ("persistent_high_values", True), + ], + ids=[ + "p97>threshold_enters", + "single_spike_max_large_but_p97_below_threshold", + "high_values_persist_trigger_entering", + ], + ) + def test_max_pred_lat_acc_uses_p97_and_threshold(self, case, should_enter): + n = len(ModelConstants.T_IDXS) + th = float(_ENTERING_PRED_LAT_ACC_TH) + + if case == "p97_just_above_threshold": + # Use the next representable float32 above threshold to avoid float32 rounding flakiness. + val = _th_above_f32(th) + pred_lat_accels = np.full(n, np.float32(val), dtype=np.float32) + + elif case == "single_spike_filtered": + pred_lat_accels = _build_single_spike_filtered(n, base=1.0) + + elif case == "persistent_high_values": + # Make enough "high" samples so p97 is driven by the persistent trend, not a single outlier. + high_count = max(2, int(np.ceil(n * 0.03)) + 1) + pred_lat_accels = np.full(n, np.float32(1.0), dtype=np.float32) + pred_lat_accels[-high_count:] = np.float32(2.0) + pred_lat_accels[-1] = np.float32(8.0) # keep one big outlier too + + else: + raise AssertionError(f"Unknown case: {case}") + + # Override model predictions so: + # predicted_lat_accels = abs(orientationRate.z) * velocity.x == pred_lat_accels + mdl = generate_modelV2() + mdl.modelV2.velocity.x = [1.0 for _ in range(n)] + mdl.modelV2.orientationRate.z = [float(x) for x in pred_lat_accels] + self.sm["modelV2"] = mdl.modelV2 + + v_ego = float(MIN_V + 5.0) + + # 1st update: disabled -> enabled + self.scc_v.update(self.sm, True, False, v_ego, 0.0, 0.0) + # 2nd update: evaluate entering condition from enabled state + self.scc_v.update(self.sm, True, False, v_ego, 0.0, 0.0) + + # Controller does percentile on numpy float64 arrays (values already quantized by capnp), + # so compute expected in float64 to match behavior and avoid interpolation/rounding deltas. + expected_p97 = float(np.percentile(pred_lat_accels.astype(np.float64), 97)) + + # allow tiny numeric differences due to float conversions/interpolation + assert np.isclose(self.scc_v.max_pred_lat_acc, expected_p97, rtol=1e-6, atol=1e-5) + + if should_enter: + # We assert entering primarily by state (this is the actual intended behavior). + assert self.scc_v.state == VisionState.entering + # Optional sanity: should be >= threshold with some margin (since we used nextafter above threshold). + assert self.scc_v.max_pred_lat_acc > th + + else: + # Difference vs np.amax(): max can be above threshold, but p97 stays below it. + assert float(np.max(pred_lat_accels)) >= th + assert self.scc_v.max_pred_lat_acc < th + assert self.scc_v.state == VisionState.enabled + # TODO-SP: mock modelV2 data to test other states diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py index d144178e93..a9d2a66227 100644 --- a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py +++ b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py @@ -90,7 +90,7 @@ class SmartCruiseControlVision: # get the maximum lat accel from the model predicted_lat_accels = rate_plan * vel_plan - self.max_pred_lat_acc = np.amax(predicted_lat_accels) + self.max_pred_lat_acc = np.percentile(predicted_lat_accels, 97) # get the maximum curve based on the current velocity v_ego = max(self.v_ego, 0.1) # ensure a value greater than 0 for calculations diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py index ca8f16fe74..b547ea96b6 100644 --- a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py @@ -20,6 +20,8 @@ class TestBlinkerPauseLateral: self.blinker_pause_lateral.enabled = True self.blinker_pause_lateral.is_metric = False self.blinker_pause_lateral.min_speed = 20 # MPH + self.blinker_pause_lateral.reengage_delay = 0 + self.blinker_pause_lateral.blinker_off_timer = 0.0 self.CS = car.CarState.new_message() self.CS.vEgo = 0 @@ -46,6 +48,18 @@ class TestBlinkerPauseLateral: } self._test_should_blinker_pause_lateral(expected_results) + def test_reengage_delay(self): + self.blinker_pause_lateral.reengage_delay = 2 # seconds + self.CS.vEgo = 4.5 # ~10 MPH + + expected_results = { + (False, False): True, + (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 diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index c0c84eac09..ad94ad35ff 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -93,6 +93,10 @@ "title": "[TIZI/TICI only] Blind Spot Detection", "description": "Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported." }, + "BlinkerLateralReengageDelay": { + "title": "Post-Blinker Delay", + "description": "Delay before lateral control resumes after the turn signal ends." + }, "BlinkerMinLateralControlSpeed": { "title": "Blinker Min Lateral Control Speed", "description": "" diff --git a/sunnypilot/sunnylink/sunnylink_state.py b/sunnypilot/sunnylink/sunnylink_state.py index 02b91c3cbc..927d041991 100644 --- a/sunnypilot/sunnylink/sunnylink_state.py +++ b/sunnypilot/sunnylink/sunnylink_state.py @@ -109,6 +109,8 @@ class SunnylinkState: self.sunnylink_dongle_id = self._params.get("SunnylinkDongleId") self._api = SunnylinkApi(self.sunnylink_dongle_id) + self._panel_open = False + self._load_initial_state() def _load_initial_state(self) -> None: @@ -166,9 +168,14 @@ class SunnylinkState: def _worker_thread(self) -> None: while self._running: - if self.is_connected(): - self._fetch_roles() - self._fetch_users() + with self._lock: + panel_open = self._panel_open + + if panel_open: + self._sm.update() + if self.is_connected(): + self._fetch_roles() + self._fetch_users() for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)): if not self._running: @@ -220,5 +227,9 @@ class SunnylinkState: else: return style.ITEM_TEXT_VALUE_COLOR + def set_settings_open(self, _open: bool) -> None: + with self._lock: + self._panel_open = _open + def __del__(self): self.stop() diff --git a/system/ui/sunnypilot/widgets/__init__.py b/system/ui/sunnypilot/widgets/__init__.py index e69de29bb2..96865e6042 100644 --- a/system/ui/sunnypilot/widgets/__init__.py +++ b/system/ui/sunnypilot/widgets/__init__.py @@ -0,0 +1,18 @@ +""" +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. +""" + + +def get_highlighted_description(params, param_name: str, descriptions: list[str]) -> str: + index = int(params.get(param_name, return_default=True)) + lines = [] + for i, desc in enumerate(descriptions): + if i == index: + lines.append(f"{desc}") + else: + lines.append(f"{desc}") + + return "
".join(lines) diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index 30a2d992a7..1dd69c24cb 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -134,6 +134,10 @@ class MultipleButtonActionSP(MultipleButtonAction): if self.param_key: self.selected_button = int(self.params.get(self.param_key, return_default=True)) self._anim_x: float | None = None + self.enabled_buttons: set[int] | None = None + + def set_enabled_buttons(self, indices: set[int] | None): + self.enabled_buttons = indices def _render(self, rect: rl.Rectangle): @@ -171,10 +175,31 @@ class MultipleButtonActionSP(MultipleButtonAction): text_x = button_x + (self.button_width - text_size.x) / 2 text_y = button_y + (style.BUTTON_HEIGHT - text_size.y) / 2 - rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color) + # Check individual button enabled state + is_button_enabled = self.enabled and (self.enabled_buttons is None or i in self.enabled_buttons) + current_text_color = text_color if is_button_enabled else style.MBC_DISABLED + + rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, current_text_color) def _handle_mouse_release(self, mouse_pos: MousePos): - MultipleButtonAction._handle_mouse_release(self, mouse_pos) + # Override parent method to check individual button enabled state + if not self.enabled: + return + + button_y = self._rect.y + (self._rect.height - style.BUTTON_HEIGHT) / 2 + for i, _ in enumerate(self.buttons): + button_x = self._rect.x + i * self.button_width + button_rect = rl.Rectangle(button_x, button_y, self.button_width, style.BUTTON_HEIGHT) + + if rl.check_collision_point_rec(mouse_pos, button_rect): + # Check if this specific button is enabled + if self.enabled_buttons is not None and i not in self.enabled_buttons: + return + + self.selected_button = i + if self.callback: + self.callback(i) + if self.param_key: self.params.put(self.param_key, self.selected_button) diff --git a/tinygrad_repo b/tinygrad_repo index 7296c74cbd..3501a71478 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 7296c74cbd2666da7dce95d7ca6dab5340653a5c +Subproject commit 3501a714785ff370cffb966a45d5f9cdf6c9ea7a diff --git a/uv.lock b/uv.lock index 8909934e9a..782c20849c 100644 --- a/uv.lock +++ b/uv.lock @@ -415,11 +415,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.3" +version = "3.24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/a8/dae62680be63cbb3ff87cfa2f51cf766269514ea5488479d42fec5aa6f3a/filelock-3.24.2.tar.gz", hash = "sha256:c22803117490f156e59fafce621f0550a7a853e2bbf4f87f112b11d469b6c81b", size = 37601, upload-time = "2026-02-16T02:50:45.614Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, + { url = "https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl", hash = "sha256:667d7dc0b7d1e1064dd5f8f8e80bdac157a6482e8d2e02cd16fd3b6b33bd6556", size = 24152, upload-time = "2026-02-16T02:50:44Z" }, ] [[package]] @@ -1144,30 +1144,30 @@ wheels = [ [[package]] name = "pillow" -version = "12.1.0" +version = "12.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, - { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, - { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, ] [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] @@ -1328,14 +1328,14 @@ wheels = [ [[package]] name = "pyee" -version = "13.0.0" +version = "13.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, ] [[package]] @@ -4055,27 +4055,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.0" +version = "0.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, - { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, - { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, - { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, - { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, - { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, - { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, - { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, - { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] [[package]] @@ -4212,26 +4212,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.15" +version = "0.0.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/25/257602d316b9333089b688a7a11b33ebc660b74e8dacf400dc3dfdea1594/ty-0.0.15.tar.gz", hash = "sha256:4f9a5b8df208c62dba56e91b93bed8b5bb714839691b8cff16d12c983bfa1174", size = 5101936, upload-time = "2026-02-05T01:06:34.922Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c3/41ae6346443eedb65b96761abfab890a48ce2aa5a8a27af69c5c5d99064d/ty-0.0.17.tar.gz", hash = "sha256:847ed6c120913e280bf9b54d8eaa7a1049708acb8824ad234e71498e8ad09f97", size = 5167209, upload-time = "2026-02-13T13:26:36.835Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/c5/35626e732b79bf0e6213de9f79aff59b5f247c0a1e3ce0d93e675ab9b728/ty-0.0.15-py3-none-linux_armv6l.whl", hash = "sha256:68e092458516c61512dac541cde0a5e4e5842df00b4e81881ead8f745ddec794", size = 10138374, upload-time = "2026-02-05T01:07:03.804Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8a/48fd81664604848f79d03879b3ca3633762d457a069b07e09fb1b87edd6e/ty-0.0.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79f2e75289eae3cece94c51118b730211af4ba5762906f52a878041b67e54959", size = 9947858, upload-time = "2026-02-05T01:06:47.453Z" }, - { url = "https://files.pythonhosted.org/packages/b6/85/c1ac8e97bcd930946f4c94db85b675561d590b4e72703bf3733419fc3973/ty-0.0.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:112a7b26e63e48cc72c8c5b03227d1db280cfa57a45f2df0e264c3a016aa8c3c", size = 9443220, upload-time = "2026-02-05T01:06:44.98Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d9/244bc02599d950f7a4298fbc0c1b25cc808646b9577bdf7a83470b2d1cec/ty-0.0.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71f62a2644972975a657d9dc867bf901235cde51e8d24c20311067e7afd44a56", size = 9949976, upload-time = "2026-02-05T01:07:01.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ab/3a0daad66798c91a33867a3ececf17d314ac65d4ae2bbbd28cbfde94da63/ty-0.0.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e48b42be2d257317c85b78559233273b655dd636fc61e7e1d69abd90fd3cba4", size = 9965918, upload-time = "2026-02-05T01:06:54.283Z" }, - { url = "https://files.pythonhosted.org/packages/39/4e/e62b01338f653059a7c0cd09d1a326e9a9eedc351a0f0de9db0601658c3d/ty-0.0.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27dd5b52a421e6871c5bfe9841160331b60866ed2040250cb161886478ab3e4f", size = 10424943, upload-time = "2026-02-05T01:07:08.777Z" }, - { url = "https://files.pythonhosted.org/packages/65/b5/7aa06655ce69c0d4f3e845d2d85e79c12994b6d84c71699cfb437e0bc8cf/ty-0.0.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76b85c9ec2219e11c358a7db8e21b7e5c6674a1fb9b6f633836949de98d12286", size = 10964692, upload-time = "2026-02-05T01:06:37.103Z" }, - { url = "https://files.pythonhosted.org/packages/13/04/36fdfe1f3c908b471e246e37ce3d011175584c26d3853e6c5d9a0364564c/ty-0.0.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e8204c61d8ede4f21f2975dce74efdb80fafb2fae1915c666cceb33ea3c90b", size = 10692225, upload-time = "2026-02-05T01:06:49.714Z" }, - { url = "https://files.pythonhosted.org/packages/13/41/5bf882649bd8b64ded5fbce7fb8d77fb3b868de1a3b1a6c4796402b47308/ty-0.0.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af87c3be7c944bb4d6609d6c63e4594944b0028c7bd490a525a82b88fe010d6d", size = 10516776, upload-time = "2026-02-05T01:06:52.047Z" }, - { url = "https://files.pythonhosted.org/packages/56/75/66852d7e004f859839c17ffe1d16513c1e7cc04bcc810edb80ca022a9124/ty-0.0.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50dccf7398505e5966847d366c9e4c650b8c225411c2a68c32040a63b9521eea", size = 9928828, upload-time = "2026-02-05T01:06:56.647Z" }, - { url = "https://files.pythonhosted.org/packages/65/72/96bc16c7b337a3ef358fd227b3c8ef0c77405f3bfbbfb59ee5915f0d9d71/ty-0.0.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bd797b8f231a4f4715110259ad1ad5340a87b802307f3e06d92bfb37b858a8f3", size = 9978960, upload-time = "2026-02-05T01:06:29.567Z" }, - { url = "https://files.pythonhosted.org/packages/a0/18/d2e316a35b626de2227f832cd36d21205e4f5d96fd036a8af84c72ecec1b/ty-0.0.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9deb7f20e18b25440a9aa4884f934ba5628ef456dbde91819d5af1a73da48af3", size = 10135903, upload-time = "2026-02-05T01:06:59.256Z" }, - { url = "https://files.pythonhosted.org/packages/02/d3/b617a79c9dad10c888d7c15cd78859e0160b8772273637b9c4241a049491/ty-0.0.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7b31b3de031255b90a5f4d9cb3d050feae246067c87130e5a6861a8061c71754", size = 10615879, upload-time = "2026-02-05T01:07:06.661Z" }, - { url = "https://files.pythonhosted.org/packages/fb/b0/2652a73c71c77296a6343217063f05745da60c67b7e8a8e25f2064167fce/ty-0.0.15-py3-none-win32.whl", hash = "sha256:9362c528ceb62c89d65c216336d28d500bc9f4c10418413f63ebc16886e16cc1", size = 9578058, upload-time = "2026-02-05T01:06:42.928Z" }, - { url = "https://files.pythonhosted.org/packages/84/6e/08a4aedebd2a6ce2784b5bc3760e43d1861f1a184734a78215c2d397c1df/ty-0.0.15-py3-none-win_amd64.whl", hash = "sha256:4db040695ae67c5524f59cb8179a8fa277112e69042d7dfdac862caa7e3b0d9c", size = 10457112, upload-time = "2026-02-05T01:06:39.885Z" }, - { url = "https://files.pythonhosted.org/packages/b3/be/1991f2bc12847ae2d4f1e3ac5dcff8bb7bc1261390645c0755bb55616355/ty-0.0.15-py3-none-win_arm64.whl", hash = "sha256:e5a98d4119e77d6136461e16ae505f8f8069002874ab073de03fbcb1a5e8bf25", size = 9937490, upload-time = "2026-02-05T01:06:32.388Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/0ef15c22a1c54b0f728ceff3f62d478dbf8b0dcf8ff7b80b954f79584f3e/ty-0.0.17-py3-none-linux_armv6l.whl", hash = "sha256:64a9a16555cc8867d35c2647c2f1afbd3cae55f68fd95283a574d1bb04fe93e0", size = 10192793, upload-time = "2026-02-13T13:27:13.943Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2c/f4c322d9cded56edc016b1092c14b95cf58c8a33b4787316ea752bb9418e/ty-0.0.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eb2dbd8acd5c5a55f4af0d479523e7c7265a88542efe73ed3d696eb1ba7b6454", size = 10051977, upload-time = "2026-02-13T13:26:57.741Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a5/43746c1ff81e784f5fc303afc61fe5bcd85d0fcf3ef65cb2cef78c7486c7/ty-0.0.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f18f5fd927bc628deb9ea2df40f06b5f79c5ccf355db732025a3e8e7152801f6", size = 9564639, upload-time = "2026-02-13T13:26:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b8/280b04e14a9c0474af574f929fba2398b5e1c123c1e7735893b4cd73d13c/ty-0.0.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5383814d1d7a5cc53b3b07661856bab04bb2aac7a677c8d33c55169acdaa83df", size = 10061204, upload-time = "2026-02-13T13:27:00.152Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d7/493e1607d8dfe48288d8a768a2adc38ee27ef50e57f0af41ff273987cda0/ty-0.0.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c20423b8744b484f93e7bf2ef8a9724bca2657873593f9f41d08bd9f83444c9", size = 10013116, upload-time = "2026-02-13T13:26:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/80/ef/22f3ed401520afac90dbdf1f9b8b7755d85b0d5c35c1cb35cf5bd11b59c2/ty-0.0.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6f5b1aba97db9af86517b911674b02f5bc310750485dc47603a105bd0e83ddd", size = 10533623, upload-time = "2026-02-13T13:26:31.449Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/744b15279a11ac7138832e3a55595706b4a8a209c9f878e3ab8e571d9032/ty-0.0.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:488bce1a9bea80b851a97cd34c4d2ffcd69593d6c3f54a72ae02e5c6e47f3d0c", size = 11069750, upload-time = "2026-02-13T13:26:48.638Z" }, + { url = "https://files.pythonhosted.org/packages/f2/be/1133c91f15a0e00d466c24f80df486d630d95d1b2af63296941f7473812f/ty-0.0.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8df66b91ec84239420985ec215e7f7549bfda2ac036a3b3c065f119d1c06825a", size = 10870862, upload-time = "2026-02-13T13:26:54.715Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/a2ed209ef215b62b2d3246e07e833081e07d913adf7e0448fc204be443d6/ty-0.0.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:002139e807c53002790dfefe6e2f45ab0e04012e76db3d7c8286f96ec121af8f", size = 10628118, upload-time = "2026-02-13T13:26:45.439Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0c/87476004cb5228e9719b98afffad82c3ef1f84334bde8527bcacba7b18cb/ty-0.0.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6c4e01f05ce82e5d489ab3900ca0899a56c4ccb52659453780c83e5b19e2b64c", size = 10038185, upload-time = "2026-02-13T13:27:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/98f0b3ba9aef53c1f0305519536967a4aa793a69ed72677b0a625c5313ac/ty-0.0.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2b226dd1e99c0d2152d218c7e440150d1a47ce3c431871f0efa073bbf899e881", size = 10047644, upload-time = "2026-02-13T13:27:05.474Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/06737bb80aa1a9103b8651d2eb691a7e53f1ed54111152be25f4a02745db/ty-0.0.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8b11f1da7859e0ad69e84b3c5ef9a7b055ceed376a432fad44231bdfc48061c2", size = 10231140, upload-time = "2026-02-13T13:27:10.844Z" }, + { url = "https://files.pythonhosted.org/packages/7c/79/e2a606bd8852383ba9abfdd578f4a227bd18504145381a10a5f886b4e751/ty-0.0.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c04e196809ff570559054d3e011425fd7c04161529eb551b3625654e5f2434cb", size = 10718344, upload-time = "2026-02-13T13:26:51.66Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2d/2663984ac11de6d78f74432b8b14ba64d170b45194312852b7543cf7fd56/ty-0.0.17-py3-none-win32.whl", hash = "sha256:305b6ed150b2740d00a817b193373d21f0767e10f94ac47abfc3b2e5a5aec809", size = 9672932, upload-time = "2026-02-13T13:27:08.522Z" }, + { url = "https://files.pythonhosted.org/packages/de/b5/39be78f30b31ee9f5a585969930c7248354db90494ff5e3d0756560fb731/ty-0.0.17-py3-none-win_amd64.whl", hash = "sha256:531828267527aee7a63e972f54e5eee21d9281b72baf18e5c2850c6b862add83", size = 10542138, upload-time = "2026-02-13T13:27:17.084Z" }, + { url = "https://files.pythonhosted.org/packages/40/b7/f875c729c5d0079640c75bad2c7e5d43edc90f16ba242f28a11966df8f65/ty-0.0.17-py3-none-win_arm64.whl", hash = "sha256:de9810234c0c8d75073457e10a84825b9cd72e6629826b7f01c7a0b266ae25b1", size = 10023068, upload-time = "2026-02-13T13:26:39.637Z" }, ] [[package]]