Compare commits

...

4 Commits

Author SHA1 Message Date
MVL 9e0d89968d Honda Clarity: brake hold fix (#1247)
* Nidec Hybrid brake hold fix

* intent fix

* bump opendbc

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-06 22:18:37 -04:00
Jason Wen fec5a97a9e docs: update policy (#1898) 2026-08-06 21:57:56 -04:00
Jason Wen 099143ad9d [mici] ui: fix swipe-down in sunnylink and models panels (#1896)
* [mici] ui: fix swipe-down in sunnylink and models panels

* update mici replay test

* [mici] ui: push model sub-views as separate widgets for correct back navigation
2026-08-05 23:22:36 -04:00
Jason Wen 6909aa95ff ui: use sunnypilot and sunnylink branding fonts (#1895)
* ui: use sunnypilot and sunnylink branding fonts

* split
2026-08-05 22:53:04 -04:00
13 changed files with 155 additions and 27 deletions
+44
View File
@@ -0,0 +1,44 @@
# AI policy
## Why this exists
We use AI tools ourselves, so this isn't an anti-AI stance. The problem is people submitting code, issues, or comments they don't actually understand. AI makes that very easy to do, and it creates real work for reviewers who have to figure out what you meant when you can't explain it yourself.
If you're not going to put effort into understanding and verifying your submission, we're not going to put effort into reviewing it.
## The rule
You are responsible for everything you submit: code, PR descriptions, issues, bug reports, comments.
1. Understand what you submit. If a reviewer asks why you did something, you answer from your own understanding, not by re-prompting. If you can't do that, don't submit it.
2. Test your change. AI gets things wrong all the time. Run it, break it, confirm it actually works.
3. Driving fixes need real evidence. Attach a dongle ID, upload logs, and include segments that show the fix working. A route hash by itself proves nothing.
4. No AI-generated media (images, diagrams, videos) in issues or PRs.
## Disclosure
If AI tools helped you write something, say so. Add an `Assisted-by:` line in your commit message:
```
Assisted-by: GitHub Copilot
Assisted-by: Claude
```
Disclosing won't count against your PR. It helps reviewers know where to look. Hiding it and getting caught will.
## How we review
Reviewers are looking at whether you understand your own change. Can you explain it? Can you respond to feedback without re-prompting? Does your PR description say why you made the change, not just list what changed?
Good code from someone who used AI and understands what they wrote is fine. How you got there doesn't matter as long as you can stand behind it.
## What happens
Submissions that don't meet this bar get closed. If it keeps happening, you get blocked.
## Maintainers
Maintainers use AI at their discretion. They've earned that through sustained contribution and they know the codebase.
+3
View File
@@ -1,3 +1,5 @@
> sunnypilot follows [commaai/openpilot](https://github.com/commaai/openpilot)'s contributing guidelines. The following applies to all contributions here.
# How to contribute # How to contribute
Our software is open source so you can solve your own problems without needing help from others. And if you solve a problem and are so kind, you can upstream it for the rest of the world to use. Check out our [post about externalization](https://blog.comma.ai/a-2020-theme-externalization/). Our software is open source so you can solve your own problems without needing help from others. And if you solve a problem and are so kind, you can upstream it for the rest of the world to use. Check out our [post about externalization](https://blog.comma.ai/a-2020-theme-externalization/).
@@ -35,6 +37,7 @@ All of these are examples of good PRs:
* **UI design**: we do not have a good review process for this yet * **UI design**: we do not have a good review process for this yet
* **New features**: We believe openpilot is mostly feature-complete, and the rest is a matter of refinement and fixing bugs. As a result of this, most feature PRs will be immediately closed, however the beauty of open source is that forks can and do offer features that upstream openpilot doesn't. * **New features**: We believe openpilot is mostly feature-complete, and the rest is a matter of refinement and fixing bugs. As a result of this, most feature PRs will be immediately closed, however the beauty of open source is that forks can and do offer features that upstream openpilot doesn't.
* **Negative expected value**: This is a class of PRs that makes an improvement, but the risk or validation costs more than the improvement. The risk can be mitigated by first getting a failing test merged. * **Negative expected value**: This is a class of PRs that makes an improvement, but the risk or validation costs more than the improvement. The risk can be mitigated by first getting a failing test merged.
* **AI-generated contributions**: see our [AI policy](AI_POLICY.md)
### First contribution ### First contribution
+3
View File
@@ -56,6 +56,9 @@ class CarSpecificEvents:
if self.CP.minEnableSpeed > 0 and CS.vEgo < 0.001: if self.CP.minEnableSpeed > 0 and CS.vEgo < 0.001:
events.add(EventName.manualRestart) events.add(EventName.manualRestart)
if CS.brakeHoldActive and CS.blockPcmEnable: # set by Nidec Hybrid which cannot resume from brakehold
events.add(EventName.belowEngageSpeed)
elif self.CP.brand == 'toyota': elif self.CP.brand == 'toyota':
# TODO: when we check for unexpected disengagement, check gear not S1, S2, S3 # TODO: when we check for unexpected disengagement, check gear not S1, S2, S3
if self.CP.openpilotLongitudinalControl: if self.CP.openpilotLongitudinalControl:
+1
View File
@@ -13,6 +13,7 @@ from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.home import HomeLayoutSP as HomeLayout
class MainState(IntEnum): class MainState(IntEnum):
@@ -13,6 +13,7 @@ from openpilot.system.ui.lib.application import gui_app
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.home import MiciHomeLayoutSP as MiciHomeLayout
ONROAD_DELAY = 2.5 # seconds ONROAD_DELAY = 2.5 # seconds
@@ -0,0 +1,68 @@
"""
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.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets.label import gui_label
BRAND_FONT_SIZE = 48
BRAND_DESC_SPACING = 12
class HomeLayoutSP(HomeLayout):
def _render_header(self):
font = gui_app.font(FontWeight.MEDIUM)
version_text_width = self.header_rect.width
if self.update_available:
version_text_width -= self.update_notif_rect.width
highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255)
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
text = tr("UPDATE")
text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
if self.alert_count > 0:
version_text_width -= self.alert_notif_rect.width
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
alert_text = trn("{} ALERT", "{} ALERTS", self.alert_count).format(self.alert_count)
text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
if self.update_available or self.alert_count > 0:
version_text_width -= SPACING * 1.5
version_right = self.header_rect.x + self.header_rect.width
version_left = version_right - version_text_width
brand = "sunnypilot"
description = self.params.get("UpdaterCurrentDescription") or ""
desc_width = 0
if description:
desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
desc_width = desc_size.x
desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height)
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
spacing = BRAND_DESC_SPACING if description else 0
brand_x = version_right - desc_width - spacing - brand_size.x
brand_rect = rl.Rectangle(max(version_left, brand_x), self.header_rect.y, brand_size.x, self.header_rect.height)
gui_label(brand_rect, brand, BRAND_FONT_SIZE, rl.WHITE, font_weight=FontWeight.AUDIOWIDE)
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
self._done_callback = done_callback self._done_callback = done_callback
self._step = 0 self._step = 0
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)) self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
self._content = [ self._content = [
{ {
@@ -0,0 +1,15 @@
"""
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.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.system.ui.lib.application import FontWeight
from openpilot.system.ui.widgets.label import UnifiedLabel
class MiciHomeLayoutSP(MiciHomeLayout):
def __init__(self):
super().__init__()
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
@@ -4,7 +4,6 @@ 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. 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. See the LICENSE.md file in the root directory for more details.
""" """
from collections.abc import Callable
import pyray as rl import pyray as rl
from openpilot.cereal import custom from openpilot.cereal import custom
@@ -48,10 +47,8 @@ class CurrentModelInfo(Widget):
self.info_text.render() self.info_text.render()
class ModelsLayoutMici(NavScroller): class ModelsLayoutMici(NavScroller):
def __init__(self, back_callback: Callable): def __init__(self):
super().__init__() super().__init__()
self.set_back_callback(back_callback)
self.original_back_callback = back_callback
self.focused_widget = None self.focused_widget = None
self.current_model_info = CurrentModelInfo() self.current_model_info = CurrentModelInfo()
@@ -85,12 +82,10 @@ class ModelsLayoutMici(NavScroller):
return folders return folders
def _show_selection_view(self, items, back_callback: Callable): def _push_selection_view(self, items):
self._scroller._items = items scroller = NavScroller()
for item in items: scroller._scroller.add_widgets(items)
item.set_touch_valid_callback(lambda: self._scroller.scroll_panel.is_touch_valid() and self._scroller.enabled) gui_app.push_widget(scroller)
self._scroller.scroll_panel.set_offset(0)
self.set_back_callback(back_callback)
def _show_folders(self): def _show_folders(self):
self.focused_widget = self.select_model_btn self.focused_widget = self.select_model_btn
@@ -112,15 +107,18 @@ class ModelsLayoutMici(NavScroller):
folder_buttons.insert(0, btn) folder_buttons.insert(0, btn)
else: else:
folder_buttons.append(btn) folder_buttons.append(btn)
self._show_selection_view(folder_buttons, self._reset_main_view) self._push_selection_view(folder_buttons)
def _pop_to_main(self):
gui_app.pop_widgets_to(self)
def _select_model(self, bundle): def _select_model(self, bundle):
ui_state.params.put("ModelManager_DownloadIndex", bundle.index) ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
self._reset_main_view() self._pop_to_main()
def _select_default(self): def _select_default(self):
ui_state.params.remove("ModelManager_ActiveBundle") ui_state.params.remove("ModelManager_ActiveBundle")
self._reset_main_view() self._pop_to_main()
def _select_folder(self, folder_name): def _select_folder(self, folder_name):
favs = ui_state.params.get("ModelManager_Favs") favs = ui_state.params.get("ModelManager_Favs")
@@ -135,13 +133,7 @@ class ModelsLayoutMici(NavScroller):
btn = BigButton(txt) btn = BigButton(txt)
btn.set_click_callback(lambda b=bundle: self._select_model(b)) btn.set_click_callback(lambda b=bundle: self._select_model(b))
btns.append(btn) btns.append(btn)
self._show_selection_view(btns, self._show_folders) self._push_selection_view(btns)
def _reset_main_view(self):
self._scroller._items = self.main_items # type: ignore[assignment] # ty: ignore[invalid-assignment]
self.set_back_callback(self.original_back_callback)
self._scroller.scroll_panel.set_offset(0)
self._scroller.scroll_to(0)
def hide_event(self): def hide_event(self):
super().hide_event() super().hide_event()
@@ -32,11 +32,11 @@ class SettingsLayoutSP(OP.SettingsLayout):
BIG_ICON_SIZE) BIG_ICON_SIZE)
self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE) self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE)
sunnylink_panel = SunnylinkLayoutMici(back_callback=gui_app.pop_widget) sunnylink_panel = SunnylinkLayoutMici()
sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55)) sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55))
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel)) sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
models_panel = ModelsLayoutMici(back_callback=gui_app.pop_widget) models_panel = ModelsLayoutMici()
models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE)) models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel)) models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
@@ -6,7 +6,6 @@ See the LICENSE.md file in the root directory for more details.
""" """
import pyray as rl import pyray as rl
from collections.abc import Callable
from openpilot.cereal import custom from openpilot.cereal import custom
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
@@ -54,9 +53,8 @@ class SunnylinkInfo(Widget):
self.sponsor_text.render() self.sponsor_text.render()
class SunnylinkLayoutMici(NavScroller): class SunnylinkLayoutMici(NavScroller):
def __init__(self, back_callback: Callable): def __init__(self):
super().__init__() super().__init__()
self.set_back_callback(back_callback)
self._restore_in_progress = False self._restore_in_progress = False
self._backup_in_progress = False self._backup_in_progress = False
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled") self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
@@ -338,8 +338,11 @@ def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None:
settings_cases: Cases = [ settings_cases: Cases = [
lambda: scroll_through_cases(toggle_cases), lambda: scroll_through_cases(toggle_cases),
None, # sunnylink (just open and close)
None, # models (just open and close)
lambda: scroll_through_cases(network_cases), lambda: scroll_through_cases(network_cases),
lambda: scroll_through_cases(device_cases), lambda: scroll_through_cases(device_cases),
lambda: script.wait(WAIT_SHORT), # software
lambda: script.wait(WAIT_SHORT), # pairing lambda: script.wait(WAIT_SHORT), # pairing
lambda: run_actions(lambda: swipe_up(height * 3), lambda: swipe_down(height * 3)), # firehose (scroll down and back up) lambda: run_actions(lambda: swipe_up(height * 3), lambda: swipe_down(height * 3)), # firehose (scroll down and back up)
lambda: scroll_through_cases(developer_cases), lambda: scroll_through_cases(developer_cases),