Merge branch 'master' into ui-better-confidence-ball

This commit is contained in:
royjr
2025-12-31 14:10:37 -05:00
20 changed files with 484 additions and 59 deletions
+2
View File
@@ -145,6 +145,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"CarParamsSPPersistent", {PERSISTENT, BYTES}},
{"CarPlatformBundle", {PERSISTENT | BACKUP, JSON}},
{"ChevronInfo", {PERSISTENT | BACKUP, INT, "4"}},
{"CompletedSunnylinkConsentVersion", {PERSISTENT, STRING, "0"}},
{"CustomAccIncrementsEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"CustomAccLongPressIncrement", {PERSISTENT | BACKUP, INT, "5"}},
{"CustomAccShortPressIncrement", {PERSISTENT | BACKUP, INT, "1"}},
@@ -155,6 +156,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"FlatUIState", {PERSISTENT | BACKUP, BOOL, "0"}},
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
{"HideVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
{"IntelligentCruiseButtonManagement", {PERSISTENT | BACKUP , BOOL}},
{"InteractivityTimeout", {PERSISTENT | BACKUP, INT, "0"}},
+38 -9
View File
@@ -11,7 +11,9 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import Label
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.version import terms_version, training_version
from openpilot.system.version import terms_version, training_version, terms_version_sp
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkOnboarding
DEBUG = False
@@ -33,6 +35,7 @@ class OnboardingState(IntEnum):
TERMS = 0
ONBOARDING = 1
DECLINE = 2
SUNNYLINK_CONSENT = 3
class TrainingGuide(Widget):
@@ -110,14 +113,14 @@ class TermsPage(Widget):
self._on_decline = on_decline
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._desc = Label(tr("You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at https://comma.ai/terms before continuing."),
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
def _render(self, _):
welcome_x = self._rect.x + 165
welcome_x = self._rect.x + 95
welcome_y = self._rect.y + 165
welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90)
self._title.render(welcome_rect)
@@ -143,7 +146,7 @@ class TermsPage(Widget):
class DeclinePage(Widget):
def __init__(self, back_callback=None):
super().__init__()
self._text = Label(tr("You must accept the Terms and Conditions in order to use sunnypilot."),
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._back_btn = Button(tr("Back"), click_callback=back_callback)
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
@@ -180,9 +183,21 @@ class OnboardingWindow(Widget):
self._training_guide: TrainingGuide | None = None
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
# sunnylink consent pages
self._accepted_terms = self._accepted_terms and ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp
self._sunnylink = SunnylinkOnboarding()
if not self._accepted_terms:
self._state = OnboardingState.TERMS
elif not self._sunnylink.completed:
self._state = OnboardingState.SUNNYLINK_CONSENT
elif not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
self._state = OnboardingState.ONBOARDING
@property
def completed(self) -> bool:
return self._accepted_terms and self._training_done
return self._accepted_terms and self._sunnylink.completed and self._training_done
def _on_terms_declined(self):
self._state = OnboardingState.DECLINE
@@ -192,8 +207,12 @@ class OnboardingWindow(Widget):
def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version)
self._state = OnboardingState.ONBOARDING
if self._training_done:
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
if not self._sunnylink.completed:
self._state = OnboardingState.SUNNYLINK_CONSENT
elif not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
gui_app.set_modal_overlay(None)
def _on_completed_training(self):
@@ -206,8 +225,18 @@ class OnboardingWindow(Widget):
if self._state == OnboardingState.TERMS:
self._terms.render(self._rect)
if self._state == OnboardingState.ONBOARDING:
self._training_guide.render(self._rect)
elif self._state == OnboardingState.SUNNYLINK_CONSENT:
self._sunnylink.render(self._rect)
if self._sunnylink.completed:
if not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
gui_app.set_modal_overlay(None)
elif self._state == OnboardingState.ONBOARDING:
if not self._training_done:
self._training_guide.render(self._rect)
else:
gui_app.set_modal_overlay(None)
elif self._state == OnboardingState.DECLINE:
self._decline_page.render(self._rect)
return -1
+38 -7
View File
@@ -17,13 +17,16 @@ from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.version import terms_version, training_version
from openpilot.system.version import terms_version, training_version, terms_version_sp
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkOnboarding
class OnboardingState(IntEnum):
TERMS = 0
ONBOARDING = 1
DECLINE = 2
SUNNYLINK_CONSENT = 3
class DriverCameraSetupDialog(DriverCameraDialog):
@@ -412,10 +415,10 @@ class TermsPage(SetupTermsPage):
super().__init__(on_accept, on_decline, "decline")
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
self._title_header = TermsHeader("terms & conditions", info_txt)
self._title_header = TermsHeader("terms of service", info_txt)
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use sunnypilot. " +
"Read the latest terms at https://comma.ai/terms before continuing.", 36,
self._terms_label = UnifiedLabel("You must accept the Terms of Service to use sunnypilot. " +
"Read the latest terms at https://sunnypilot.ai/terms before continuing.", 36,
FontWeight.ROMAN)
@property
@@ -449,6 +452,18 @@ class OnboardingWindow(Widget):
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
# sunnylink consent pages
self._accepted_terms = self._accepted_terms and ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp
self._sunnylink = SunnylinkOnboarding()
if not self._accepted_terms:
self._state = OnboardingState.TERMS
elif not self._sunnylink.completed:
self._state = OnboardingState.SUNNYLINK_CONSENT
elif not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
self._state = OnboardingState.ONBOARDING
def show_event(self):
super().show_event()
device.set_override_interactive_timeout(300)
@@ -459,7 +474,7 @@ class OnboardingWindow(Widget):
@property
def completed(self) -> bool:
return self._accepted_terms and self._training_done
return self._accepted_terms and self._sunnylink.completed and self._training_done
def _on_terms_declined(self):
self._state = OnboardingState.DECLINE
@@ -473,7 +488,13 @@ class OnboardingWindow(Widget):
def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version)
self._state = OnboardingState.ONBOARDING
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
if not self._sunnylink.completed:
self._state = OnboardingState.SUNNYLINK_CONSENT
elif not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
self.close()
def _on_completed_training(self):
ui_state.params.put("CompletedTrainingVersion", training_version)
@@ -482,8 +503,18 @@ class OnboardingWindow(Widget):
def _render(self, _):
if self._state == OnboardingState.TERMS:
self._terms.render(self._rect)
elif self._state == OnboardingState.SUNNYLINK_CONSENT:
self._sunnylink.render(self._rect)
if self._sunnylink.completed:
if not self._training_done:
self._state = OnboardingState.ONBOARDING
else:
self.close()
elif self._state == OnboardingState.ONBOARDING:
self._training_guide.render(self._rect)
if not self._training_done:
self._training_guide.render(self._rect)
else:
self.close()
elif self._state == OnboardingState.DECLINE:
self._decline_page.render(self._rect)
return -1
@@ -16,6 +16,7 @@ from openpilot.common.transformations.orientation import rot_from_euler
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.onroad.hud_renderer import HudRendererSP as HudRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.driver_state import DriverStateRendererSP as DriverStateRenderer
from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP
@@ -0,0 +1,116 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import Label
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
class SunnylinkConsentPage(Widget):
def __init__(self, done_callback=None):
super().__init__()
self._done_callback = done_callback
self._step = 0
self._title = Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._content = [
{
"text": tr("sunnylink enables secured remote access to your comma device from anywhere, " +
"including settings management, remote monitoring, real-time dashboard, etc."),
"primary_btn": tr("Enable"),
"secondary_btn": tr("Disable"),
"highlight_primary": True
},
{
"text": tr("sunnylink is designed to be enabled as part of sunnypilot's core functionality. " +
"If sunnylink is disabled, features such as settings management, remote monitoring, " +
"real-time dashboards will be unavailable."),
"secondary_btn": tr("Back"),
"danger_btn": tr("Disable"),
"highlight_primary": True
}
]
self._primary_btn = Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable"))
self._secondary_btn = Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary"))
self._danger_btn = Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable"))
def _handle_choice(self, choice):
if choice == "enable":
ui_state.params.put_bool("SunnylinkEnabled", True)
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
if self._done_callback:
self._done_callback()
elif choice == "secondary":
if self._step == 0:
self._step = 1
elif self._step == 1:
self._step = 0
elif choice == "disable":
ui_state.params.put_bool("SunnylinkEnabled", False)
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
if self._done_callback:
self._done_callback()
def _render(self, _):
step_data = self._content[self._step]
welcome_x = self._rect.x + 95
welcome_y = self._rect.y + 165
welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90)
self._title.render(welcome_rect)
desc_x = welcome_x
desc_y = welcome_y + 120
desc_rect = rl.Rectangle(desc_x, desc_y, self._rect.width - desc_x, self._rect.height - desc_y - 250)
desc_label = Label(step_data["text"], font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
desc_label.render(desc_rect)
btn_y = self._rect.y + self._rect.height - 160 - 45
if "danger_btn" in step_data:
btn_width = (self._rect.width - 45 * 3) / 2
self._secondary_btn.set_text(step_data["secondary_btn"])
self._secondary_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
self._danger_btn.set_text(step_data["danger_btn"])
self._danger_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
else:
btn_width = (self._rect.width - 45 * 3) / 2
self._secondary_btn.set_text(step_data["secondary_btn"])
self._secondary_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
self._primary_btn.set_text(step_data["primary_btn"])
self._primary_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
return -1
class SunnylinkOnboarding:
def __init__(self):
self.consent_page = SunnylinkConsentPage(done_callback=self._on_done)
self.consent_done: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") in {sunnylink_consent_version, sunnylink_consent_declined}
@property
def completed(self) -> bool:
return self.consent_done
def _on_done(self):
self.consent_done = True
def render(self, rect):
if not self.consent_done:
self.consent_page.render(rect)
@@ -202,7 +202,7 @@ class DeviceLayoutSP(DeviceLayout):
self._scroller._items.remove(self._always_offroad_btn)
if ui_state.is_offroad() and not always_offroad:
self._scroller._items.insert(len(self._scroller._items) - 1, self._always_offroad_btn)
elif not ui_state.is_offroad():
else:
self._scroller._items.insert(0, self._always_offroad_btn)
# Quiet Mode button
@@ -4,23 +4,23 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from cereal import custom
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
from openpilot.system.ui.sunnypilot.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.list_view import button_item, dual_button_item
from openpilot.system.ui.widgets.list_view import dual_button_item
from openpilot.system.ui.widgets.scroller_tici import Scroller, LineSeparator
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
import pyray as rl
if gui_app.sunnypilot_ui():
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp as button_item
from openpilot.system.version import sunnylink_consent_version
class SunnylinkHeader(Widget):
@@ -160,14 +160,14 @@ class SunnylinkLayout(Widget):
self._sunnylink_description = SunnylinkDescriptionItem()
self._sunnylink_description.set_visible(False)
self._sponsor_btn = button_item(
self._sponsor_btn = button_item_sp(
title=tr("Sponsor Status"),
button_text=tr("SPONSOR"),
description=tr(
"Become a sponsor of sunnypilot to get early access to sunnylink features when they become available."),
callback=lambda: self._handle_pair_btn(False)
)
self._pair_btn = button_item(
self._pair_btn = button_item_sp(
title=tr("Pair GitHub Account"),
button_text=tr("Not Paired"),
description=tr(
@@ -302,6 +302,22 @@ class SunnylinkLayout(Widget):
self._restore_btn.set_text(tr("Restore Settings"))
def _sunnylink_toggle_callback(self, state: bool):
sl_consent: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") == sunnylink_consent_version
sl_enabled: bool = ui_state.params.get_bool("SunnylinkEnabled")
if state and not sl_consent and not sl_enabled:
def on_consent_done():
enabled = ui_state.params.get_bool("SunnylinkEnabled")
self._update_description(enabled)
gui_app.set_modal_overlay(None)
sl_terms_dlg = SunnylinkConsentPage(done_callback=on_consent_done)
gui_app.set_modal_overlay(sl_terms_dlg)
else:
ui_state.params.put_bool("SunnylinkEnabled", state)
self._update_description(state)
def _update_description(self, state: bool):
if state:
description = tr(
"Welcome back!! We're excited to see you've enabled sunnylink again!")
@@ -0,0 +1,97 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.slider import SmallSlider
from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
from openpilot.selfdrive.ui.ui_state import ui_state
class SunnylinkConsentPage(SetupTermsPage):
def __init__(self, on_accept=None, on_decline=None, left_text: str = "disable", right_text: str = "enable"):
super().__init__(on_accept, on_decline, left_text, continue_text=right_text)
self._title_header = TermsHeader("sunnylink",
gui_app.texture("../../sunnypilot/selfdrive/assets/logo.png", 66, 60))
self._terms_label = UnifiedLabel("sunnylink enables secured remote access to your comma device from anywhere, " +
"including settings management, remote monitoring, real-time dashboard, etc.",
36, FontWeight.ROMAN)
@property
def _content_height(self):
return self._terms_label.rect.y + self._terms_label.rect.height - self._scroll_panel.get_offset()
def _render(self, _):
super()._render(_)
return -1
def _render_content(self, scroll_offset):
self._title_header.set_position(self._rect.x + 16, self._rect.y + 12 + scroll_offset)
self._title_header.render()
self._terms_label.render(rl.Rectangle(
self._rect.x + 16,
self._title_header.rect.y + self._title_header.rect.height + self.ITEM_SPACING,
self._rect.width - 100,
self._terms_label.get_content_height(int(self._rect.width - 100)),
))
class SunnylinkConsentDisableConfirmPage(SunnylinkConsentPage):
def __init__(self, on_accept=None, on_decline=None):
super().__init__(on_accept=on_decline, on_decline=on_accept, left_text="enable", right_text="disable")
# we flip the continue & disable buttons to use slider for disable
self._continue_slider = True
self._continue_button = SmallSlider("disable", confirm_callback=on_decline)
self._scroll_panel.set_enabled(lambda: not self._continue_button.is_pressed)
self._title_header = TermsHeader("disable sunnylink?",
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
self._terms_label = UnifiedLabel("sunnylink is designed to be enabled as part of sunnypilot's core functionality. " +
"If sunnylink is disabled, features such as settings management, " +
"remote monitoring, real-time dashboards will be unavailable.",
36, FontWeight.ROMAN)
class SunnylinkOnboarding:
def __init__(self):
self.consent_done: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") in {sunnylink_consent_version, sunnylink_consent_declined}
self.disable_confirm = False
self.consent_page = SunnylinkConsentPage(on_decline=self._on_decline, on_accept=self._on_accept)
self.confirm_page = SunnylinkConsentDisableConfirmPage(on_decline=self._on_confirm_decline, on_accept=self._on_accept)
@property
def completed(self) -> bool:
return self.consent_done
def _on_accept(self):
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
ui_state.params.put_bool("SunnylinkEnabled", True)
self.consent_done = True
def _on_decline(self):
self.disable_confirm = True
def _on_confirm_decline(self):
ui_state.params.put_bool("SunnylinkEnabled", False)
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
self.consent_done = True
def render(self, rect):
if self.consent_done:
return
if self.disable_confirm:
self.confirm_page.render(rect)
else:
self.consent_page.render(rect)
@@ -8,16 +8,17 @@ from collections.abc import Callable
import pyray as rl
from cereal import custom
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
from openpilot.system.ui.lib.application import gui_app, MousePos
from openpilot.system.ui.widgets import NavWidget
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, MousePos
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import NavWidget
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
class SunnylinkLayoutMici(NavWidget):
@@ -28,9 +29,9 @@ class SunnylinkLayoutMici(NavWidget):
self._backup_in_progress = False
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
self._sunnylink_toggle = BigToggle(text="",
self._sunnylink_toggle = BigToggle(text=tr("enable sunnylink"),
initial_state=self._sunnylink_enabled,
toggle_callback=SunnylinkLayoutMici._sunnylink_toggle_callback)
toggle_callback=self._sunnylink_toggle_callback)
self._sunnylink_sponsor_button = SunnylinkPairBigButton(sponsor_pairing=False)
self._sunnylink_pair_button = SunnylinkPairBigButton(sponsor_pairing=True)
self._backup_btn = BigButton(tr("backup settings"), "", "")
@@ -38,7 +39,7 @@ class SunnylinkLayoutMici(NavWidget):
self._restore_btn = BigButton(tr("restore settings"), "", "")
self._restore_btn.set_click_callback(lambda: self._handle_backup_restore_btn(restore=True))
self._sunnylink_uploader_toggle = BigToggle(text=tr("sunnylink uploader"), initial_state=False,
toggle_callback=SunnylinkLayoutMici._sunnylink_uploader_callback)
toggle_callback=self._sunnylink_uploader_callback)
self._scroller = Scroller([
self._sunnylink_toggle,
@@ -51,8 +52,8 @@ class SunnylinkLayoutMici(NavWidget):
def _update_state(self):
super()._update_state()
self._sunnylink_enabled = ui_state.sunnylink_enabled
self._sunnylink_toggle.set_text(tr("enable sunnylink"))
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
self._sunnylink_toggle.set_checked(self._sunnylink_enabled)
self._sunnylink_pair_button.set_visible(self._sunnylink_enabled)
self._sunnylink_sponsor_button.set_visible(self._sunnylink_enabled)
self._backup_btn.set_visible(self._sunnylink_enabled)
@@ -83,7 +84,25 @@ class SunnylinkLayoutMici(NavWidget):
@staticmethod
def _sunnylink_toggle_callback(state: bool):
ui_state.params.put_bool("SunnylinkEnabled", state)
sl_consent: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") == sunnylink_consent_version
sl_enabled: bool = ui_state.params.get("SunnylinkEnabled")
def sl_terms_accepted():
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
ui_state.params.put_bool("SunnylinkEnabled", True)
gui_app.set_modal_overlay(None)
def sl_terms_declined():
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
ui_state.params.put_bool("SunnylinkEnabled", False)
gui_app.set_modal_overlay(None)
if state and not sl_consent and not sl_enabled:
sl_terms_dlg = SunnylinkConsentPage(on_accept=sl_terms_accepted, on_decline=sl_terms_declined)
gui_app.set_modal_overlay(sl_terms_dlg)
else:
ui_state.params.put_bool("SunnylinkEnabled", state)
ui_state.update_params()
@staticmethod
@@ -22,6 +22,7 @@ class DeveloperUiRenderer(Widget):
DEV_UI_RIGHT = 1
DEV_UI_BOTTOM = 2
DEV_UI_BOTH = 3
BOTTOM_BAR_HEIGHT = 61
def __init__(self):
super().__init__()
@@ -43,6 +44,12 @@ class DeveloperUiRenderer(Widget):
self.bearing_elem = BearingDegElement()
self.altitude_elem = AltitudeElement()
@staticmethod
def get_bottom_dev_ui_offset():
if ui_state.developer_ui in (DeveloperUiRenderer.DEV_UI_BOTTOM, DeveloperUiRenderer.DEV_UI_BOTH):
return DeveloperUiRenderer.BOTTOM_BAR_HEIGHT
return 0
def _update_state(self) -> None:
self.dev_ui_mode = ui_state.developer_ui
@@ -78,10 +85,11 @@ class DeveloperUiRenderer(Widget):
]
if controls_state.lateralControlState.which() == 'torqueState':
elements.append(self.desired_lat_accel_elem.update(sm, ui_state.is_metric))
elements.append(self.actual_lat_accel_elem.update(sm, ui_state.is_metric))
else:
elements.append(self.desired_steer_elem.update(sm, ui_state.is_metric))
elements.append(self.actual_lat_accel_elem.update(sm, ui_state.is_metric))
current_y = y
for element in elements:
current_y += self._draw_right_dev_ui_element(x, current_y, element)
@@ -105,7 +113,7 @@ class DeveloperUiRenderer(Widget):
if element.unit:
units_height = measure_text_cached(self._font_bold, element.unit, unit_size, 0).x
units_x = x + container_width - 10
units_x = x + container_width
units_y = y + (value_size / 2) + (units_height / 2)
rl.draw_text_pro(self._font_bold, element.unit, rl.Vector2(units_x, units_y), rl.Vector2(0, 0), -90.0, unit_size, 0, rl.WHITE)
@@ -143,22 +151,35 @@ class DeveloperUiRenderer(Widget):
if sm.valid['gpsLocationExternal'] or sm.valid['gpsLocation']:
elements.append(self.altitude_elem.update(sm, ui_state.is_metric))
current_x = int(rect.x + 90)
center_y = y + bar_height // 2
for element in elements:
current_x += self._draw_bottom_dev_ui_element(current_x, center_y, element)
if not elements:
return
def _draw_bottom_dev_ui_element(self, x: int, y: int, element: UiElement) -> int:
font_size = 38
element_widths = []
for element in elements:
element.measure(self._font_bold, font_size)
element_widths.append(element.total_width)
label_text = f"{element.label} "
label_width = measure_text_cached(self._font_bold, label_text, font_size, 0).x
rl.draw_text_ex(self._font_bold, label_text, rl.Vector2(x, y - font_size // 2), font_size, 0, rl.WHITE)
total_element_width = sum(element_widths)
num_gaps = len(elements) + 1
available_width = rect.width
gap_width = (available_width - total_element_width) / num_gaps
value_width = measure_text_cached(self._font_bold, element.value, font_size, 0).x
rl.draw_text_ex(self._font_bold, element.value, rl.Vector2(x + label_width + 10, y - font_size // 2), font_size, 0, element.color)
center_y = y + bar_height // 2
current_x = rect.x + gap_width
for i, element in enumerate(elements):
element_center_x = int(current_x + element_widths[i] / 2)
self._draw_bottom_dev_ui_element(element_center_x, center_y, element)
current_x += element_widths[i] + gap_width
def _draw_bottom_dev_ui_element(self, center_x: int, y: int, element: UiElement) -> None:
font_size = 38
start_x = center_x - element.total_width / 2
rl.draw_text_ex(self._font_bold, element.label_text, rl.Vector2(start_x, y - font_size // 2), font_size, 0, rl.WHITE)
rl.draw_text_ex(self._font_bold, element.val_text, rl.Vector2(start_x + element.label_width, y - font_size // 2), font_size, 0, element.color)
if element.unit:
rl.draw_text_ex(self._font_bold, element.unit, rl.Vector2(x + label_width + value_width + 20, y - font_size // 2), font_size, 0, rl.WHITE)
return 400
rl.draw_text_ex(self._font_bold, element.unit_text, rl.Vector2(start_x + element.label_width + element.val_width, y - font_size // 2),
font_size, 0, rl.WHITE)
@@ -10,12 +10,33 @@ from dataclasses import dataclass
from openpilot.common.constants import CV
from openpilot.system.ui.lib.text_measure import measure_text_cached
@dataclass
class UiElement:
value: str
label: str
unit: str
color: rl.Color
val_text: str = ""
label_text: str = ""
unit_text: str = ""
val_width: float = 0.0
label_width: float = 0.0
unit_width: float = 0.0
total_width: float = 0.0
def measure(self, font, font_size: int):
self.label_text = f"{self.label} "
self.val_text = self.value
self.unit_text = f" {self.unit}" if self.unit else ""
self.label_width = measure_text_cached(font, self.label_text, font_size, 0).x
self.val_width = measure_text_cached(font, self.val_text, font_size, 0).x
self.unit_width = measure_text_cached(font, self.unit_text, font_size, 0).x if self.unit else 0
self.total_width = self.label_width + self.val_width + self.unit_width
class LeadInfoElement:
@@ -0,0 +1,49 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import numpy as np
from openpilot.selfdrive.ui import UI_BORDER_SIZE
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer, BTN_SIZE, ARC_LENGTH
from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer
class DriverStateRendererSP(DriverStateRenderer):
def __init__(self):
super().__init__()
self.dev_ui_offset = DeveloperUiRenderer.get_bottom_dev_ui_offset()
def _pre_calculate_drawing_elements(self):
"""Pre-calculate all drawing elements based on the current rectangle"""
# Calculate icon position (bottom-left or bottom-right)
width, height = self._rect.width, self._rect.height
offset = UI_BORDER_SIZE + BTN_SIZE // 2
self.position_x = self._rect.x + (width - offset if self.is_rhd else offset)
self.position_y = self._rect.y + height - offset - self.dev_ui_offset
# Pre-calculate the face lines positions
positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y])
for i in range(len(positioned_keypoints)):
self.face_lines[i].x = positioned_keypoints[i][0]
self.face_lines[i].y = positioned_keypoints[i][1]
# Calculate arc dimensions based on head rotation
delta_x = -self.driver_pose_sins[1] * ARC_LENGTH / 2.0 # Horizontal movement
delta_y = -self.driver_pose_sins[0] * ARC_LENGTH / 2.0 # Vertical movement
# Horizontal arc
h_width = abs(delta_x)
self.h_arc_data = self._calculate_arc_data(
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
)
# Vertical arc
v_height = abs(delta_y)
self.v_arc_data = self._calculate_arc_data(
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
)
+3 -1
View File
@@ -13,7 +13,7 @@ if "RECORD_OUTPUT" not in os.environ:
os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ["RECORD_OUTPUT"])
from openpilot.common.params import Params
from openpilot.system.version import terms_version, training_version
from openpilot.system.version import terms_version, training_version, terms_version_sp, sunnylink_consent_version
from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
@@ -45,6 +45,8 @@ def setup_state():
params.put("CompletedTrainingVersion", training_version)
params.put("DongleId", "test123456789")
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30")
params.put("HasAcceptedTermsSP", terms_version_sp)
params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
return None
@@ -18,7 +18,7 @@ from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.test.helpers import with_processes
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.system.updated.updated import parse_release_notes
from openpilot.system.version import terms_version, training_version
from openpilot.system.version import terms_version, training_version, terms_version_sp, sunnylink_consent_version
AlertSize = log.SelfdriveState.AlertSize
AlertStatus = log.SelfdriveState.AlertStatus
@@ -378,6 +378,8 @@ def create_screenshots():
# Set terms and training version (to skip onboarding)
params.put("HasAcceptedTerms", terms_version)
params.put("CompletedTrainingVersion", training_version)
params.put("HasAcceptedTermsSP", terms_version_sp)
params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
if name == "homescreen_paired":
params.put("PrimeType", 0) # NONE
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:66b3aefa108dd0c7f64205a11e424430c318e6fd06de31b5550d0b9d05616e6a
size 19035
+5 -1
View File
@@ -42,10 +42,14 @@ METADATA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__f
params = Params()
# Parameters that should never be remotely modified for security reasons
# Parameters that should never be remotely modified
BLOCKED_PARAMS = {
"CompletedSunnylinkConsentVersion",
"CompletedTrainingVersion",
"GithubUsername", # Could grant SSH access
"GithubSshKeys", # Direct SSH key injection
"HasAcceptedTerms",
"HasAcceptedTermsSP",
}
@@ -165,6 +165,10 @@
"title": "Chevron Info",
"description": ""
},
"CompletedSunnylinkConsentVersion": {
"title": "Completed sunnylink Consent Version",
"description": ""
},
"CompletedTrainingVersion": {
"title": "Completed Training Version",
"description": ""
@@ -353,6 +357,10 @@
"title": "Has Accepted Terms",
"description": ""
},
"HasAcceptedTermsSP": {
"title": "Has Accepted sunnypilot Terms",
"description": ""
},
"HideVEgoUI": {
"title": "Hide vEgo UI",
"description": ""
+2 -1
View File
@@ -23,7 +23,7 @@ from openpilot.system.statsd import statlog
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware.power_monitoring import PowerMonitoring
from openpilot.system.hardware.fan_controller import TiciFanController
from openpilot.system.version import terms_version, training_version, get_build_metadata
from openpilot.system.version import terms_version, training_version, get_build_metadata, terms_version_sp
ThermalStatus = log.DeviceState.ThermalStatus
NetworkType = log.DeviceState.NetworkType
@@ -310,6 +310,7 @@ def hardware_thread(end_event, hw_queue) -> None:
startup_conditions["no_excessive_actuation"] = params.get("Offroad_ExcessiveActuation") is None
startup_conditions["not_uninstalling"] = not params.get_bool("DoUninstall")
startup_conditions["accepted_terms"] = params.get("HasAcceptedTerms") == terms_version
startup_conditions["accepted_terms_sp"] = params.get("HasAcceptedTermsSP") == terms_version_sp
# with 2% left, we killall, otherwise the phone will take a long time to boot
startup_conditions["free_space"] = msg.deviceState.freeSpacePercent > 2
+3
View File
@@ -30,6 +30,9 @@ BUILD_METADATA_FILENAME = "build.json"
training_version: str = "0.2.0"
terms_version: str = "2"
terms_version_sp: str = "1.0"
sunnylink_consent_version: str = "1.0"
sunnylink_consent_declined: str = "-1"
def get_version(path: str = BASEDIR) -> str: