Reimplement sunnypilot Terms of Service & sunnylink Consent Screens (#1633)

* tos reimpl

* nah

* simpler

* check consent on sunnylink panel - mici

* slight cleanup

* rename

* keep it off

* decouple

* more rename

* more decouple

* a bit more

* fix state

* decouple more

* a bit more

* wrong type

* rearrange

* don't do that

* final

* lint

* include

* more

---------

Co-authored-by: nayan <nayan8teen@gmail.com>
This commit is contained in:
Jason Wen
2025-12-31 00:08:36 -05:00
committed by GitHub
parent 70386c6b00
commit fb8f46cba9
15 changed files with 376 additions and 43 deletions

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"}},
@@ -154,6 +155,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
{"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"}},

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

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

View File

@@ -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)

View File

@@ -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!")

View File

View File

@@ -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)

View File

@@ -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

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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:66b3aefa108dd0c7f64205a11e424430c318e6fd06de31b5550d0b9d05616e6a
size 19035

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",
}

View File

@@ -165,6 +165,10 @@
"title": "Chevron Info",
"description": ""
},
"CompletedSunnylinkConsentVersion": {
"title": "Completed sunnylink Consent Version",
"description": ""
},
"CompletedTrainingVersion": {
"title": "Completed Training Version",
"description": ""
@@ -349,6 +353,10 @@
"title": "Has Accepted Terms",
"description": ""
},
"HasAcceptedTermsSP": {
"title": "Has Accepted sunnypilot Terms",
"description": ""
},
"HideVEgoUI": {
"title": "Hide vEgo UI",
"description": ""

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

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: