|
|
|
@ -7,7 +7,8 @@ from openpilot.common.params import Params |
|
|
|
|
from openpilot.common.swaglog import cloudlog |
|
|
|
|
from openpilot.selfdrive.ui.ui_state import ui_state |
|
|
|
|
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID |
|
|
|
|
from openpilot.system.ui.lib.application import gui_app, FontWeight |
|
|
|
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE |
|
|
|
|
from openpilot.system.ui.lib.text_measure import measure_text_cached |
|
|
|
|
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel |
|
|
|
|
from openpilot.system.ui.lib.wrap_text import wrap_text |
|
|
|
|
from openpilot.system.ui.widgets import Widget |
|
|
|
@ -21,7 +22,7 @@ DESCRIPTION = ( |
|
|
|
|
) |
|
|
|
|
INSTRUCTIONS = ( |
|
|
|
|
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n" |
|
|
|
|
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n" |
|
|
|
|
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n\n" |
|
|
|
|
+ "Frequently Asked Questions\n\n" |
|
|
|
|
+ "Does it matter how or where I drive? Nope, just drive as you normally would.\n\n" |
|
|
|
|
+ "Do all of my segments get pulled in Firehose Mode? No, we selectively pull a subset of your segments.\n\n" |
|
|
|
@ -43,6 +44,7 @@ class FirehoseLayout(Widget): |
|
|
|
|
self.params = Params() |
|
|
|
|
self.segment_count = self._get_segment_count() |
|
|
|
|
self.scroll_panel = GuiScrollPanel() |
|
|
|
|
self._content_height = 0 |
|
|
|
|
|
|
|
|
|
self.running = True |
|
|
|
|
self.update_thread = threading.Thread(target=self._update_loop, daemon=True) |
|
|
|
@ -69,88 +71,61 @@ class FirehoseLayout(Widget): |
|
|
|
|
|
|
|
|
|
def _render(self, rect: rl.Rectangle): |
|
|
|
|
# Calculate content dimensions |
|
|
|
|
content_width = rect.width - 80 |
|
|
|
|
content_height = self._calculate_content_height(int(content_width)) |
|
|
|
|
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height) |
|
|
|
|
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height) |
|
|
|
|
|
|
|
|
|
# Handle scrolling and render with clipping |
|
|
|
|
scroll_offset = self.scroll_panel.update(rect, content_rect) |
|
|
|
|
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) |
|
|
|
|
self._render_content(rect, scroll_offset) |
|
|
|
|
self._content_height = self._render_content(rect, scroll_offset) |
|
|
|
|
rl.end_scissor_mode() |
|
|
|
|
|
|
|
|
|
def _calculate_content_height(self, content_width: int) -> int: |
|
|
|
|
height = 80 # Top margin |
|
|
|
|
|
|
|
|
|
# Title |
|
|
|
|
height += 100 + 40 |
|
|
|
|
|
|
|
|
|
# Description |
|
|
|
|
desc_font = gui_app.font(FontWeight.NORMAL) |
|
|
|
|
desc_lines = wrap_text(desc_font, DESCRIPTION, 45, content_width) |
|
|
|
|
height += len(desc_lines) * 45 + 40 |
|
|
|
|
|
|
|
|
|
# Status section |
|
|
|
|
height += 32 # Separator |
|
|
|
|
status_text, _ = self._get_status() |
|
|
|
|
status_lines = wrap_text(gui_app.font(FontWeight.BOLD), status_text, 60, content_width) |
|
|
|
|
height += len(status_lines) * 60 + 20 |
|
|
|
|
|
|
|
|
|
# Contribution count (if available) |
|
|
|
|
if self.segment_count > 0: |
|
|
|
|
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far." |
|
|
|
|
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 52, content_width) |
|
|
|
|
height += len(contrib_lines) * 52 + 20 |
|
|
|
|
|
|
|
|
|
# Instructions section |
|
|
|
|
height += 32 # Separator |
|
|
|
|
inst_lines = wrap_text(gui_app.font(FontWeight.NORMAL), INSTRUCTIONS, 40, content_width) |
|
|
|
|
height += len(inst_lines) * 40 + 40 # Bottom margin |
|
|
|
|
|
|
|
|
|
return height |
|
|
|
|
|
|
|
|
|
def _render_content(self, rect: rl.Rectangle, scroll_offset: float): |
|
|
|
|
def _render_content(self, rect: rl.Rectangle, scroll_offset: float) -> int: |
|
|
|
|
x = int(rect.x + 40) |
|
|
|
|
y = int(rect.y + 40 + scroll_offset) |
|
|
|
|
w = int(rect.width - 80) |
|
|
|
|
|
|
|
|
|
# Title |
|
|
|
|
# Title (centered) |
|
|
|
|
title_font = gui_app.font(FontWeight.MEDIUM) |
|
|
|
|
rl.draw_text_ex(title_font, TITLE, rl.Vector2(x, y), 100, 0, rl.WHITE) |
|
|
|
|
y += 140 |
|
|
|
|
text_width = measure_text_cached(title_font, TITLE, 100).x |
|
|
|
|
title_x = rect.x + (rect.width - text_width) / 2 |
|
|
|
|
rl.draw_text_ex(title_font, TITLE, rl.Vector2(title_x, y), 100, 0, rl.WHITE) |
|
|
|
|
y += 200 |
|
|
|
|
|
|
|
|
|
# Description |
|
|
|
|
y = self._draw_wrapped_text(x, y, w, DESCRIPTION, gui_app.font(FontWeight.NORMAL), 45, rl.WHITE) |
|
|
|
|
y += 40 |
|
|
|
|
y += 40 + 20 |
|
|
|
|
|
|
|
|
|
# Separator |
|
|
|
|
rl.draw_rectangle(x, y, w, 2, self.GRAY) |
|
|
|
|
y += 30 |
|
|
|
|
y += 30 + 20 |
|
|
|
|
|
|
|
|
|
# Status |
|
|
|
|
status_text, status_color = self._get_status() |
|
|
|
|
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color) |
|
|
|
|
y += 20 |
|
|
|
|
y += 20 + 20 |
|
|
|
|
|
|
|
|
|
# Contribution count (if available) |
|
|
|
|
if self.segment_count > 0: |
|
|
|
|
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far." |
|
|
|
|
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE) |
|
|
|
|
y += 20 |
|
|
|
|
y += 20 + 20 |
|
|
|
|
|
|
|
|
|
# Separator |
|
|
|
|
rl.draw_rectangle(x, y, w, 2, self.GRAY) |
|
|
|
|
y += 30 |
|
|
|
|
y += 30 + 20 |
|
|
|
|
|
|
|
|
|
# Instructions |
|
|
|
|
self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY) |
|
|
|
|
y = self._draw_wrapped_text(x, y, w, INSTRUCTIONS, gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY) |
|
|
|
|
|
|
|
|
|
# bottom margin + remove effect of scroll offset |
|
|
|
|
return int(round(y - self.scroll_panel.offset + 40)) |
|
|
|
|
|
|
|
|
|
def _draw_wrapped_text(self, x, y, width, text, font, size, color): |
|
|
|
|
wrapped = wrap_text(font, text, size, width) |
|
|
|
|
def _draw_wrapped_text(self, x, y, width, text, font, font_size, color): |
|
|
|
|
wrapped = wrap_text(font, text, font_size, width) |
|
|
|
|
for line in wrapped: |
|
|
|
|
rl.draw_text_ex(font, line, rl.Vector2(x, y), size, 0, color) |
|
|
|
|
y += size |
|
|
|
|
return y |
|
|
|
|
rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color) |
|
|
|
|
y += font_size * FONT_SCALE |
|
|
|
|
return round(y) |
|
|
|
|
|
|
|
|
|
def _get_status(self) -> tuple[str, rl.Color]: |
|
|
|
|
network_type = ui_state.sm["deviceState"].networkType |
|
|
|
|