""" Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ """ import os import time import pyray as rl from collections.abc import Callable from iqpilot.common.basedir import BASEDIR from iqpilot.common.params import Params from iqpilot.common.time_helpers import system_time_valid from iqpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigCircleButton from iqpilot.selfdrive.ui.mici.widgets.stock_dialog import BigDialog, BigConfirmationDialog from iqpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog from iqpilot.selfdrive.ui.mici.widgets.stock_pairing_dialog import PairingDialog from iqpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog from iqpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage from iqpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from iqpilot.system.ui.lib.multilang import multilang, tr from iqpilot.system.ui.widgets import Widget from iqpilot.selfdrive.ui.ui_state import device, ui_state from iqpilot.system.ui.widgets.label import UnifiedLabel from iqpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer from iqpilot.konn3kt.registration import UNREGISTERED_DONGLE_ID class ReviewTermsPage(TermsPage, NavScroller): """TermsPage with NavWidget swipe-to-dismiss for reviewing in device settings.""" def __init__(self): super().__init__(on_accept=self.dismiss, on_decline=self.dismiss) self._continue_button.set_visible(False) self._back_button.set_visible(False) class ReviewTrainingGuide(TrainingGuide): def show_event(self): super().show_event() device.set_override_interactive_timeout(300) def hide_event(self): super().hide_event() device.set_override_interactive_timeout(None) ui_state.params.put_bool("IsDriverViewEnabled", False) class MiciFccModal(NavRawScrollPanel): def __init__(self, file_path: str | None = None, text: str | None = None): super().__init__() self._content = HtmlRenderer(file_path=file_path, text=text) self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64) def _render(self, rect: rl.Rectangle): content_height = self._content.get_total_height(int(rect.width)) content_height += self._fcc_logo.height + 20 scroll_content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height) scroll_offset = round(self._scroll_panel.update(rect, scroll_content_rect.height)) fcc_pos = rl.Vector2(rect.x + 20, rect.y + 20 + scroll_offset) scroll_content_rect.y += scroll_offset + self._fcc_logo.height + 20 self._content.render(scroll_content_rect) rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE) def _engaged_confirmation_click(callback: Callable, action_text: str, icon: rl.Texture, exit_on_confirm: bool = True, red: bool = False): if not ui_state.engaged: def confirm_callback(): # Check engaged again in case it changed while the dialog was open # TODO: if true, we stay on the dialog if not exit_on_confirm until normal onroad timeout if not ui_state.engaged: callback() gui_app.push_widget(BigConfirmationDialog(tr("slide to\n{}").format(action_text.lower()), icon, confirm_callback, exit_on_confirm=exit_on_confirm, red=red)) else: gui_app.push_widget(BigDialog("", tr("Disengage to {}").format(action_text))) class EngagedConfirmationCircleButton(BigCircleButton): def __init__(self, title: str, icon: rl.Texture, callback: Callable[[], None], exit_on_confirm: bool = True, red: bool = False, icon_offset: tuple[int, int] = (0, 0)): super().__init__(icon, red, icon_offset) self.set_click_callback(lambda: _engaged_confirmation_click(callback, title, icon, exit_on_confirm=exit_on_confirm, red=red)) class EngagedConfirmationButton(BigButton): def __init__(self, text: str, action_text: str, icon: rl.Texture, callback: Callable[[], None], exit_on_confirm: bool = True, red: bool = False): super().__init__(text, "", icon) self.set_click_callback(lambda: _engaged_confirmation_click(callback, action_text, icon, exit_on_confirm=exit_on_confirm, red=red)) class ForceOffroadButton(BigButton): def __init__(self): self._offroad_icon = gui_app.texture("icons/iq/square-parking.png", 64, 64) super().__init__(tr("force\noffroad"), "", self._offroad_icon) self.set_press_effect_enabled(False) self._label.set_font_size(40) self._label.set_line_height(0.95) self.set_click_callback(self._on_click) self._sync_from_params() def _forced(self) -> bool: return ui_state.params.get_bool("IQAlwaysOffroad") def _on_click(self): forced = self._forced() action = tr("disable force offroad") if forced else tr("force offroad") _engaged_confirmation_click(lambda: ui_state.params.put_bool("IQAlwaysOffroad", not forced), action, self._offroad_icon, exit_on_confirm=False) def _sync_from_params(self): self.set_text(tr("disable\nforce offroad") if self._forced() else tr("force\noffroad")) def _update_state(self): super()._update_state() self._sync_from_params() class DeviceInfoLayoutMici(Widget): def __init__(self): super().__init__() self.set_rect(rl.Rectangle(0, 0, 360, 180)) params = Params() subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) max_width = int(self._rect.width - 20) self._dongle_id_label = UnifiedLabel(tr("device ID"), 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) self._dongle_id_text_label = UnifiedLabel(params.get("DongleId") or 'N/A', 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, wrap_text=False) self._serial_number_label = UnifiedLabel(tr("serial"), 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) self._serial_number_text_label = UnifiedLabel(params.get("HardwareSerial") or 'N/A', 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, wrap_text=False) def _render(self, _): self._dongle_id_label.set_position(self._rect.x + 20, self._rect.y - 10) self._dongle_id_label.render() self._dongle_id_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25) self._dongle_id_text_label.render() self._serial_number_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30) self._serial_number_label.render() self._serial_number_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25) self._serial_number_text_label.render() class PairBigButton(BigButton): """Konn3kt connect button: logo + live connection-status dot. Uses the new accent box style.""" KONN3KT_ONLINE_NS = 80_000_000_000 # 80 seconds in nanoseconds STATUS_ONLINE = rl.Color(0x86, 0xFF, 0x4E, 255) STATUS_OFFLINE = rl.Color(0xC9, 0x22, 0x31, 255) def __init__(self): super().__init__("konn3kt", tr("pair in app"), gui_app.texture("icons_mici/settings/konn3kt_icon.png", 56, 56)) def _get_label_font_size(self): return 64 def _is_konn3kt_online(self) -> bool: last_ping = ui_state.sm['deviceState'].lastAthenaPingTime return last_ping != 0 and (time.monotonic_ns() - last_ping) < self.KONN3KT_ONLINE_NS def _update_state(self): super()._update_state() if ui_state.prime_state.is_paired(): self.set_value(tr("online") if self._is_konn3kt_online() else tr("offline")) else: self.set_value(tr("pair in app")) def _render(self, _): super()._render(_) if ui_state.prime_state.is_paired(): color = self.STATUS_ONLINE if self._is_konn3kt_online() else self.STATUS_OFFLINE rl.draw_circle(int(self._rect.x + 30), int(self._rect.y + 30), 9, color) def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) if ui_state.prime_state.is_paired(): return dlg: BigDialog | PairingDialog if not system_time_valid(): dlg = BigDialog("", tr("Please connect to Wi-Fi to complete initial pairing.")) elif UNREGISTERED_DONGLE_ID == (ui_state.params.get("DongleId") or UNREGISTERED_DONGLE_ID): dlg = BigDialog("", tr("Device must be registered with Konn3kt to pair.")) else: dlg = PairingDialog() gui_app.push_widget(dlg) class DeviceLayoutMici(NavScroller): def __init__(self): super().__init__() self._fcc_dialog: HtmlModal | None = None def power_off_callback(): ui_state.params.put_bool("DoShutdown", True) def reboot_callback(): ui_state.params.put_bool("DoReboot", True) def reset_calibration_callback(): params = ui_state.params params.remove("CalibrationParams") params.remove("LiveTorqueParameters") params.remove("LiveParameters") params.remove("LiveParametersV2") params.remove("LiveDelay") params.put_bool("OnroadCycleRequested", True) reset_calibration_btn = EngagedConfirmationButton(tr("reset calibration"), tr("reset"), gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64), reset_calibration_callback) reboot_btn = EngagedConfirmationCircleButton(tr("reboot"), gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70), reboot_callback, exit_on_confirm=False) self._power_off_btn = EngagedConfirmationCircleButton(tr("power off"), gui_app.texture("icons_mici/settings/device/power.png", 64, 66), power_off_callback, exit_on_confirm=False, red=True) self._power_off_btn.set_visible(lambda: not ui_state.ignition) regulatory_btn = BigButton(tr("regulatory info"), "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64)) regulatory_btn.set_click_callback(self._on_regulatory) driver_cam_btn = BigButton(tr("driver\ncamera preview"), "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64)) driver_cam_btn.set_click_callback(lambda: gui_app.push_widget(DriverCameraDialog())) driver_cam_btn.set_enabled(lambda: ui_state.is_offroad()) review_training_guide_btn = BigButton(tr("review\ntraining guide"), "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64)) review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTrainingGuide(completed_callback=lambda: gui_app.pop_widgets_to(self)))) review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad()) terms_btn = BigButton(tr("terms &\nconditions"), "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64)) terms_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTermsPage())) language_btn = BigButton(tr("change\nlanguage"), multilang.codes.get(multilang.language, ""), gui_app.texture("icons_mici/settings/device/language.png", 64, 64)) language_btn.set_click_callback(self._on_change_language) self._scroller.add_widgets([ DeviceInfoLayoutMici(), PairBigButton(), review_training_guide_btn, driver_cam_btn, language_btn, terms_btn, regulatory_btn, reset_calibration_btn, ForceOffroadButton(), reboot_btn, self._power_off_btn, ]) def _on_change_language(self): names = list(multilang.languages.keys()) if not names: return current = multilang.codes.get(multilang.language) dlg = BigMultiOptionDialog(names, current if current in names else names[0], right_btn="check", right_btn_callback=lambda: self._apply_language(dlg.get_selected_option())) gui_app.push_widget(dlg) def _apply_language(self, name: str): code = multilang.languages.get(name) if code and code != multilang.language: multilang.change_language(code) def _on_regulatory(self): if not self._fcc_dialog: self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "iqpilot/selfdrive/assets/offroad/mici_fcc.html")) gui_app.push_widget(self._fcc_dialog)