diff --git a/selfdrive/ui/sunnypilot/layouts/settings/settings.py b/selfdrive/ui/sunnypilot/layouts/settings/settings.py
index b2f7ceb41d..0d30fdb6dd 100644
--- a/selfdrive/ui/sunnypilot/layouts/settings/settings.py
+++ b/selfdrive/ui/sunnypilot/layouts/settings/settings.py
@@ -12,7 +12,7 @@ from openpilot.selfdrive.ui.layouts.settings import settings as OP
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
-from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
+from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from openpilot.system.ui.lib.application import gui_app, MousePos
from openpilot.system.ui.lib.multilang import tr_noop
@@ -115,7 +115,7 @@ class SettingsLayoutSP(OP.SettingsLayout):
OP.PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUISP(wifi_manager), icon="icons/network.png"),
OP.PanelType.SUNNYLINK: PanelInfo(tr_noop("sunnylink"), SunnylinkLayout(), icon="icons/wifi_strength_full.png"),
OP.PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"),
- OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_software.png"),
+ OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayoutSP(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_software.png"),
OP.PanelType.MODELS: PanelInfo(tr_noop("Models"), ModelsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_models.png"),
OP.PanelType.STEERING: PanelInfo(tr_noop("Steering"), SteeringLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"),
OP.PanelType.CRUISE: PanelInfo(tr_noop("Cruise"), CruiseLayout(), icon="icons/speed_limit.png"),
diff --git a/selfdrive/ui/sunnypilot/layouts/settings/software.py b/selfdrive/ui/sunnypilot/layouts/settings/software.py
new file mode 100644
index 0000000000..b890dd5b5b
--- /dev/null
+++ b/selfdrive/ui/sunnypilot/layouts/settings/software.py
@@ -0,0 +1,96 @@
+"""
+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 os
+
+from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
+from openpilot.selfdrive.ui.ui_state import ui_state
+from openpilot.system.hardware import HARDWARE
+from openpilot.system.ui.lib.application import gui_app
+from openpilot.system.ui.lib.multilang import tr, tr_noop
+from openpilot.system.ui.widgets import DialogResult
+from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
+
+from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
+from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder
+
+
+DESCRIPTIONS = {
+ 'disable_updates_offroad': tr_noop(
+ "When enabled, automatic software updates will be off.
This requires a reboot to take effect."
+ ),
+ 'disable_updates_onroad': tr_noop(
+ "Please enable \"Always Offroad\" mode or turn off the vehicle to adjust these toggles."
+ )
+}
+
+
+class SoftwareLayoutSP(SoftwareLayout):
+ def __init__(self):
+ super().__init__()
+ self.disable_updates_toggle = toggle_item_sp(
+ lambda: tr("Disable Updates"),
+ description="",
+ initial_state=ui_state.params.get_bool("DisableUpdates"),
+ callback=self._on_disable_updates_toggled,
+ )
+ self._scroller.add_widget(self.disable_updates_toggle)
+
+ def _handle_reboot(self, result):
+ if result == DialogResult.CONFIRM:
+ ui_state.params.put_bool("DisableUpdates", self.disable_updates_toggle.action_item.get_state())
+ ui_state.params.put_bool("DoReboot", True)
+ else:
+ self.disable_updates_toggle.action_item.set_state(ui_state.params.get_bool("DisableUpdates"))
+
+ def _on_disable_updates_toggled(self, enabled):
+ dialog = ConfirmDialog(tr("System reboot required for changes to take effect. Reboot now?"), tr("Reboot"))
+ gui_app.set_modal_overlay(dialog, callback=self._handle_reboot)
+
+ def _on_select_branch(self):
+ current_git_branch = ui_state.params.get("GitBranch") or ""
+ branches_str = ui_state.params.get("UpdaterAvailableBranches") or ""
+ branches = [b for b in branches_str.split(",") if b]
+ current_target = ui_state.params.get("UpdaterTargetBranch") or ""
+ top_level_branches = [current_git_branch, "release-mici", "release-tizi", "staging", "dev", "master"]
+
+ if HARDWARE.get_device_type() == "tici":
+ top_level_branches = ["release-tici", "staging-tici"]
+ branches = [b for b in branches if b.endswith("-tici")]
+
+ top_level_nodes = [TreeNode(b, {'display_name': b}) for b in top_level_branches if b in branches]
+ remaining_branches = [b for b in branches if b not in top_level_branches]
+ prebuilt_nodes = [TreeNode(b, {'display_name': b}) for b in remaining_branches if b.endswith("-prebuilt")]
+ non_prebuilt_nodes = [TreeNode(b, {'display_name': b}) for b in remaining_branches if not b.endswith("-prebuilt")]
+
+ folders = [
+ TreeFolder("", top_level_nodes),
+ TreeFolder("Prebuilt Branches", prebuilt_nodes),
+ TreeFolder("Non-Prebuilt Branches", non_prebuilt_nodes),
+ ]
+
+ def _on_branch_selected(result):
+ if result == DialogResult.CONFIRM and self._branch_dialog is not None:
+ selection = self._branch_dialog.selection_ref
+ if selection:
+ ui_state.params.put("UpdaterTargetBranch", selection)
+ self._branch_btn.action_item.set_value(selection)
+ os.system("pkill -SIGUSR1 -f system.updated.updated")
+ self._branch_dialog = None
+
+ self._branch_dialog = TreeOptionDialog(tr("Select a branch"), folders, current_target, "",
+ on_exit=_on_branch_selected)
+
+ gui_app.set_modal_overlay(self._branch_dialog, callback=_on_branch_selected)
+
+ def _update_state(self):
+ super()._update_state()
+ show_advanced = ui_state.params.get_bool("ShowAdvancedControls")
+ self.disable_updates_toggle.action_item.set_enabled(ui_state.is_offroad())
+ self.disable_updates_toggle.set_visible(show_advanced)
+
+ disable_updates_desc = tr(DESCRIPTIONS["disable_updates_offroad"] if ui_state.is_offroad() else DESCRIPTIONS["disable_updates_onroad"])
+ self.disable_updates_toggle.set_description(disable_updates_desc)
diff --git a/system/ui/sunnypilot/widgets/tree_dialog.py b/system/ui/sunnypilot/widgets/tree_dialog.py
index cfc0a5caf1..2dd5e3a2dc 100644
--- a/system/ui/sunnypilot/widgets/tree_dialog.py
+++ b/system/ui/sunnypilot/widgets/tree_dialog.py
@@ -78,7 +78,7 @@ class TreeItemWidget(Button):
class TreeOptionDialog(MultiOptionDialog):
def __init__(self, title, folders, current_ref="", fav_param="", option_font_weight=FontWeight.MEDIUM, search_prompt=None,
get_folders_fn=None, on_exit=None, display_func=None, search_funcs=None, search_title=None, search_subtitle=None):
- super().__init__(title, [], "", option_font_weight)
+ super().__init__(title, [], current_ref, option_font_weight)
self.folders = folders
self.selection_ref = current_ref
self.fav_param = fav_param
@@ -101,6 +101,23 @@ class TreeOptionDialog(MultiOptionDialog):
self.search_dialog = None
self._search_pressed = False
+ self.selection_node = None
+ # Try to match by ref, by display text, or fall back to "Default" when no ref is set
+ for folder in self.folders:
+ for node in folder.nodes:
+ display = self.display_func(node)
+ if (
+ node.ref == current_ref or
+ display == current_ref or
+ (not current_ref and node.ref == "Default")
+ ):
+ self.selection = display
+ self.current = display
+ self.selection_node = node
+ break
+ if self.selection_node is not None:
+ break
+
self._build_visible_items()
def _on_search_confirm(self, result, text):
@@ -142,6 +159,17 @@ class TreeOptionDialog(MultiOptionDialog):
def _build_visible_items(self, reset_scroll=True):
self.visible_items = []
+
+ # Pinned selected item at the very top (if any)
+ if getattr(self, "selection_node", None) is not None:
+ node = self.selection_node
+ display = self.display_func(node)
+ self.selection = self.current = display
+ favorite_cb = (lambda node_ref=node: self._toggle_favorite(node_ref)) if self.fav_param and node.ref != "Default" else None
+ self.visible_items.append(TreeItemWidget(self.display_func(node), node.ref, False, 0,
+ lambda node_ref=node: self._select_node(node_ref),
+ favorite_cb, node.ref in self.favorites, is_expanded=True))
+
for folder in self.folders:
nodes = [node for node in folder.nodes if not self.query or search_from_list(self.query, [search_func(node) for search_func in self.search_funcs])]
if not nodes and self.query:
@@ -152,10 +180,15 @@ class TreeOptionDialog(MultiOptionDialog):
lambda folder_ref=folder: self._toggle_folder(folder_ref)))
if expanded:
for node in nodes:
+ # Skip duplicate root-level item for the selected node
+ if self.selection_node is not None and node.ref == self.selection_node.ref and not folder.folder:
+ continue
+
favorite_cb = (lambda node_ref=node: self._toggle_favorite(node_ref)) if self.fav_param and node.ref != "Default" else None
self.visible_items.append(TreeItemWidget(self.display_func(node), node.ref, False, 1 if folder.folder else 0,
lambda node_ref=node: self._select_node(node_ref),
favorite_cb, node.ref in self.favorites, is_expanded=expanded))
+
self.option_buttons = self.visible_items
self.options = [item.text for item in self.visible_items]
self.scroller._items = self.visible_items