Skip to content

Commit 3d4ecc0

Browse files
Merge branch 'master' into better-html-parser
2 parents d1c0b00 + b3eba70 commit 3d4ecc0

37 files changed

+325
-194
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ dependencies = [
7272
"zstandard",
7373

7474
# ui
75+
"raylib < 5.5.0.3", # TODO: unpin when they fix https://github.com/electronstudio/raylib-python-cffi/issues/186
7576
"qrcode",
77+
"mapbox-earcut",
7678
]
7779

7880
[project.optional-dependencies]
@@ -119,7 +121,6 @@ dev = [
119121
"tabulate",
120122
"types-requests",
121123
"types-tabulate",
122-
"raylib < 5.5.0.3", # TODO: unpin when they fix https://github.com/electronstudio/raylib-python-cffi/issues/186
123124
]
124125

125126
tools = [

selfdrive/SConscript

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ SConscript(['controls/lib/lateral_mpc_lib/SConscript'])
33
SConscript(['controls/lib/longitudinal_mpc_lib/SConscript'])
44
SConscript(['locationd/SConscript'])
55
SConscript(['modeld/SConscript'])
6-
SConscript(['ui/SConscript'])
6+
if GetOption('extras'):
7+
SConscript(['ui/SConscript'])

selfdrive/test/test_onroad.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
TEST_DURATION = 25
3333
LOG_OFFSET = 8
3434

35-
MAX_TOTAL_CPU = 300. # total for all 8 cores
35+
MAX_TOTAL_CPU = 315. # total for all 8 cores
3636
PROCS = {
3737
# Baseline CPU usage by process
3838
"selfdrive.controls.controlsd": 16.0,
@@ -42,7 +42,7 @@
4242
"./encoderd": 13.0,
4343
"./camerad": 10.0,
4444
"selfdrive.controls.plannerd": 8.0,
45-
"./ui": 18.0,
45+
"selfdrive.ui.ui": 24.0,
4646
"system.sensord.sensord": 13.0,
4747
"selfdrive.controls.radard": 2.0,
4848
"selfdrive.modeld.modeld": 22.0,
@@ -215,7 +215,7 @@ def test_ui_timings(self):
215215
print(result)
216216

217217
assert max(ts) < 250.
218-
assert np.mean(ts) < 10.
218+
assert np.mean(ts) < 20. # TODO: ~6-11ms, increase consistency
219219
#self.assertLess(np.std(ts), 5.)
220220

221221
# some slow frames are expected since camerad/modeld can preempt ui

selfdrive/ui/layouts/home.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def __init__(self):
3636
self.update_alert = UpdateAlert()
3737
self.offroad_alert = OffroadAlert()
3838

39+
self._layout_widgets = {HomeLayoutState.UPDATE: self.update_alert, HomeLayoutState.ALERTS: self.offroad_alert}
40+
3941
self.current_state = HomeLayoutState.HOME
4042
self.last_refresh = 0
4143
self.settings_callback: callable | None = None
@@ -66,11 +68,19 @@ def show_event(self):
6668
def _setup_callbacks(self):
6769
self.update_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
6870
self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
71+
self._exp_mode_button.set_click_callback(lambda: self.settings_callback() if self.settings_callback else None)
6972

7073
def set_settings_callback(self, callback: Callable):
7174
self.settings_callback = callback
7275

7376
def _set_state(self, state: HomeLayoutState):
77+
# propagate show/hide events
78+
if state != self.current_state:
79+
if state in self._layout_widgets:
80+
self._layout_widgets[state].show_event()
81+
if self.current_state in self._layout_widgets:
82+
self._layout_widgets[self.current_state].hide_event()
83+
7484
self.current_state = state
7585

7686
def _render(self, rect: rl.Rectangle):
@@ -138,9 +148,9 @@ def _render_header(self):
138148
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
139149

140150
text = "UPDATE"
141-
text_width = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE).x
142-
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_width) // 2
143-
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
151+
text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
152+
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
153+
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
144154
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
145155

146156
# Alert notification button
@@ -152,9 +162,9 @@ def _render_header(self):
152162
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
153163

154164
alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}"
155-
text_width = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE).x
156-
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_width) // 2
157-
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - HEAD_BUTTON_FONT_SIZE) // 2
165+
text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
166+
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
167+
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2
158168
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
159169

160170
# Version text (right aligned)
@@ -201,11 +211,11 @@ def _refresh(self):
201211

202212
# Show panels on transition from no alert/update to any alerts/update
203213
if not update_available and not alerts_present:
204-
self.current_state = HomeLayoutState.HOME
214+
self._set_state(HomeLayoutState.HOME)
205215
elif update_available and ((not self._prev_update_available) or (not alerts_present and self.current_state == HomeLayoutState.ALERTS)):
206-
self.current_state = HomeLayoutState.UPDATE
216+
self._set_state(HomeLayoutState.UPDATE)
207217
elif alerts_present and ((not self._prev_alerts_present) or (not update_available and self.current_state == HomeLayoutState.UPDATE)):
208-
self.current_state = HomeLayoutState.ALERTS
218+
self._set_state(HomeLayoutState.ALERTS)
209219

210220
self.update_available = update_available
211221
self.alert_count = alert_count

selfdrive/ui/layouts/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def _setup_callbacks(self):
5050
on_flag=self._on_bookmark_clicked,
5151
open_settings=lambda: self.open_settings(PanelType.TOGGLES))
5252
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
53+
self._layouts[MainState.HOME].set_settings_callback(lambda: self.open_settings(PanelType.TOGGLES))
5354
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
5455
self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked)
5556
device.add_interactive_timeout_callback(self._set_mode_for_state)

selfdrive/ui/layouts/settings/developer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(self):
3838
description=DESCRIPTIONS["enable_adb"],
3939
initial_state=self._params.get_bool("AdbEnabled"),
4040
callback=self._on_enable_adb,
41+
enabled=ui_state.is_offroad,
4142
)
4243

4344
# SSH enable toggle + SSH key management
@@ -54,6 +55,7 @@ def __init__(self):
5455
description="",
5556
initial_state=self._params.get_bool("JoystickDebugMode"),
5657
callback=self._on_joystick_debug_mode,
58+
enabled=ui_state.is_offroad,
5759
)
5860

5961
self._long_maneuver_toggle = toggle_item(
@@ -68,6 +70,7 @@ def __init__(self):
6870
description=DESCRIPTIONS["alpha_longitudinal"],
6971
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
7072
callback=self._on_alpha_long_enabled,
73+
enabled=ui_state.is_offroad,
7174
)
7275

7376
items = [
@@ -88,6 +91,7 @@ def _render(self, rect):
8891
self._scroller.render(rect)
8992

9093
def show_event(self):
94+
self._scroller.show_event()
9195
self._update_toggles()
9296

9397
def _update_toggles(self):
@@ -153,7 +157,7 @@ def confirm_callback(result: int):
153157
self._alpha_long_toggle.action_item.set_state(False)
154158

155159
# show confirmation dialog
156-
content = (f"<h2>{self._alpha_long_toggle.title}</h2><br>" +
160+
content = (f"<h1>{self._alpha_long_toggle.title}</h1><br>" +
157161
f"<p>{self._alpha_long_toggle.description}</p>")
158162

159163
dlg = ConfirmDialog(content, "Enable", rich=True)

selfdrive/ui/layouts/settings/device.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def __init__(self):
4242
items = self._initialize_items()
4343
self._scroller = Scroller(items, line_separator=True, spacing=0)
4444

45+
ui_state.add_offroad_transition_callback(self._offroad_transition)
46+
4547
def _initialize_items(self):
4648
dongle_id = self._params.get("DongleId") or "N/A"
4749
serial = self._params.get("HardwareSerial") or "N/A"
@@ -52,20 +54,29 @@ def _initialize_items(self):
5254
self._reset_calib_btn = button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt)
5355
self._reset_calib_btn.set_description_opened_callback(self._update_calib_description)
5456

57+
self._power_off_btn = dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt)
58+
5559
items = [
5660
text_item("Dongle ID", dongle_id),
5761
text_item("Serial", serial),
5862
self._pair_device_btn,
5963
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
6064
self._reset_calib_btn,
61-
button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide),
62-
regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory),
63-
button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
64-
dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt),
65+
button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide, enabled=ui_state.is_offroad),
66+
regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory, enabled=ui_state.is_offroad),
67+
# TODO: implement multilang
68+
# button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad),
69+
self._power_off_btn,
6570
]
6671
regulatory_btn.set_visible(TICI)
6772
return items
6873

74+
def _offroad_transition(self):
75+
self._power_off_btn.action_item.right_button.set_visible(ui_state.is_offroad())
76+
77+
def show_event(self):
78+
self._scroller.show_event()
79+
6980
def _render(self, rect):
7081
self._scroller.render(rect)
7182

selfdrive/ui/layouts/settings/firehose.py

Lines changed: 29 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from openpilot.common.swaglog import cloudlog
88
from openpilot.selfdrive.ui.ui_state import ui_state
99
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
10-
from openpilot.system.ui.lib.application import gui_app, FontWeight
10+
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
11+
from openpilot.system.ui.lib.text_measure import measure_text_cached
1112
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
1213
from openpilot.system.ui.lib.wrap_text import wrap_text
1314
from openpilot.system.ui.widgets import Widget
@@ -21,7 +22,7 @@
2122
)
2223
INSTRUCTIONS = (
2324
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
24-
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n"
25+
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n\n"
2526
+ "Frequently Asked Questions\n\n"
2627
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n"
2728
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n"
@@ -43,12 +44,16 @@ def __init__(self):
4344
self.params = Params()
4445
self.segment_count = self._get_segment_count()
4546
self.scroll_panel = GuiScrollPanel()
47+
self._content_height = 0
4648

4749
self.running = True
4850
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
4951
self.update_thread.start()
5052
self.last_update_time = 0
5153

54+
def show_event(self):
55+
self.scroll_panel.set_offset(0)
56+
5257
def _get_segment_count(self) -> int:
5358
stats = self.params.get(self.PARAM_KEY)
5459
if not stats:
@@ -66,88 +71,61 @@ def __del__(self):
6671

6772
def _render(self, rect: rl.Rectangle):
6873
# Calculate content dimensions
69-
content_width = rect.width - 80
70-
content_height = self._calculate_content_height(int(content_width))
71-
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
74+
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height)
7275

7376
# Handle scrolling and render with clipping
7477
scroll_offset = self.scroll_panel.update(rect, content_rect)
7578
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
76-
self._render_content(rect, scroll_offset)
79+
self._content_height = self._render_content(rect, scroll_offset)
7780
rl.end_scissor_mode()
7881

79-
def _calculate_content_height(self, content_width: int) -> int:
80-
height = 80 # Top margin
81-
82-
# Title
83-
height += 100 + 40
84-
85-
# Description
86-
desc_font = gui_app.font(FontWeight.NORMAL)
87-
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width)
88-
height += len(desc_lines) * 45 + 40
89-
90-
# Status section
91-
height += 32 # Separator
92-
status_text, _ = self._get_status()
93-
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width)
94-
height += len(status_lines) * 60 + 20
95-
96-
# Contribution count (if available)
97-
if self.segment_count > 0:
98-
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
99-
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width)
100-
height += len(contrib_lines) * 52 + 20
101-
102-
# Instructions section
103-
height += 32 # Separator
104-
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width)
105-
height += len(inst_lines) * 40 + 40 # Bottom margin
106-
107-
return height
108-
109-
def _render_content(self, rect: rl.Rectangle, scroll_offset: float):
82+
def _render_content(self, rect: rl.Rectangle, scroll_offset: float) -> int:
11083
x = int(rect.x + 40)
11184
y = int(rect.y + 40 + scroll_offset)
11285
w = int(rect.width - 80)
11386

114-
# Title
87+
# Title (centered)
11588
title_font = gui_app.font(FontWeight.MEDIUM)
116-
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE)
117-
y += 140
89+
text_width = measure_text_cached(title_font, TITLE, 100).x
90+
title_x = rect.x + (rect.width - text_width) / 2
91+
rl.draw_text_ex(title_font, TITLE, rl.Vector2(title_x, y), 100, 0, rl.WHITE)
92+
y += 200
11893

11994
# Description
12095
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE)
121-
y += 40
96+
y += 40 + 20
12297

12398
# Separator
12499
rl.draw_rectangle(x, y, w, 2, self.GRAY)
125-
y += 30
100+
y += 30 + 20
126101

127102
# Status
128103
status_text, status_color = self._get_status()
129104
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
130-
y += 20
105+
y += 20 + 20
131106

132107
# Contribution count (if available)
133108
if self.segment_count > 0:
134109
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far."
135110
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
136-
y += 20
111+
y += 20 + 20
137112

138113
# Separator
139114
rl.draw_rectangle(x, y, w, 2, self.GRAY)
140-
y += 30
115+
y += 30 + 20
141116

142117
# Instructions
143-
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
118+
y = self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
119+
120+
# bottom margin + remove effect of scroll offset
121+
return int(round(y - self.scroll_panel.offset + 40))
144122

145-
def _draw_wrapped_text(self, x, y, width, text, font, size, color):
146-
wrapped = wrap_text(font, text, size, width)
123+
def _draw_wrapped_text(self, x, y, width, text, font, font_size, color):
124+
wrapped = wrap_text(font, text, font_size, width)
147125
for line in wrapped:
148-
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color)
149-
y += size
150-
return y
126+
rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color)
127+
y += font_size * FONT_SCALE
128+
return round(y)
151129

152130
def _get_status(self) -> tuple[str, rl.Color]:
153131
network_type = ui_state.sm["deviceState"].networkType

selfdrive/ui/layouts/settings/software.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,15 @@ def _init_items(self):
6464
self._version_item,
6565
self._download_btn,
6666
self._install_btn,
67-
button_item("Target Branch", "SELECT", callback=self._on_select_branch),
67+
# TODO: implement branch switching
68+
# button_item("Target Branch", "SELECT", callback=self._on_select_branch),
6869
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall),
6970
]
7071
return items
7172

73+
def show_event(self):
74+
self._scroller.show_event()
75+
7276
def _render(self, rect):
7377
self._scroller.render(rect)
7478

0 commit comments

Comments
 (0)