This commit is contained in:
firestarsdog
2026-09-30 02:23:29 -04:00
parent 11900616bf
commit 1e6b221d53
2 changed files with 152 additions and 13 deletions
@@ -31,6 +31,9 @@ HEADER_ICON_SIZE = 34
HEADER_FONT_SIZE = 28
VALUE_FONT_SIZE = 96
UNIT_FONT_SIZE = 28
PAUSE_ICON_WIDTH = 12
PAUSE_ICON_HEIGHT = 14
PAUSE_ICON_GAP = 8
OFFSET_FONT_SIZE = 22
OFFSET_PILL_HEIGHT = 30
CONFIRMATION_COLOR = rl.Color(188, 132, 255, 255)
@@ -81,6 +84,7 @@ class UnifiedSpeedWidget(LayoutWidget):
self._slc_enabled = False
self._presentation: UnifiedSpeedPresentation | None = None
self._show_max = False
self._pedal_override = False
self._snapshot_frame: int | None = None
def _refresh_snapshot(self) -> None:
@@ -94,6 +98,11 @@ class UnifiedSpeedWidget(LayoutWidget):
self.hud_renderer.is_cruise_available and
not ui_state.starpilot_toggles.get("hide_max_speed", False)
)
self._pedal_override = (
self.hud_renderer.is_cruise_set and ui_state.engaged and
ui_state.sm.valid.get("carState", False) and ui_state.sm.alive.get("carState", False) and
ui_state.sm.recv_frame["carState"] >= ui_state.started_frame and ui_state.sm["carState"].gasPressed
)
self._presentation = resolve_unified_speed(
self._show_max, self.hud_renderer.is_cruise_set, self.hud_renderer.set_speed,
self._slc_state, self._slc_enabled, ui_state.is_metric,
@@ -158,17 +167,32 @@ class UnifiedSpeedWidget(LayoutWidget):
rl.draw_rectangle_rounded_lines_ex(pill, roundness_for(pill, 17), 8, 2, OFFSET_COLOR)
self._draw_centered_text(text, pill, y + (pill.height - text_size.y) / 2, OFFSET_FONT_SIZE, OFFSET_COLOR)
@staticmethod
def _max_header_color(active_side: str, cruise_set: bool) -> rl.Color:
def _draw_unit(self, bounds: rl.Rectangle, y: float) -> None:
text = tr(self._presentation.unit_text)
color = COLORS.WHITE_TRANSLUCENT
if self._pedal_override:
text_size = measure_text_cached(self._font_semi_bold, text, UNIT_FONT_SIZE)
text_shift = (PAUSE_ICON_WIDTH + PAUSE_ICON_GAP) / 2
icon_x = bounds.x + (bounds.width - text_size.x) / 2 - text_shift
icon_y = y + (text_size.y - PAUSE_ICON_HEIGHT) / 2
bar_width = PAUSE_ICON_WIDTH / 3
for x in (icon_x, icon_x + 2 * bar_width):
rl.draw_rectangle_rec(rl.Rectangle(x, icon_y, bar_width, PAUSE_ICON_HEIGHT), OFFSET_COLOR)
bounds = rl.Rectangle(bounds.x + text_shift, bounds.y, bounds.width, bounds.height)
color = COLORS.DISENGAGED
self._draw_centered_text(text, bounds, y, UNIT_FONT_SIZE, color)
def _max_header_color(self, active_side: str, cruise_set: bool) -> rl.Color:
if self._pedal_override:
return COLORS.DISENGAGED
if cruise_set and ui_state.status == UIStatus.ENGAGED and active_side in ("max", "shared"):
return COLORS.ENGAGED
if cruise_set and ui_state.status in (UIStatus.DISENGAGED, UIStatus.OVERRIDE):
return COLORS.DISENGAGED
return COLORS.GREY
@staticmethod
def _limit_header_color(active_side: str, overridden: bool) -> rl.Color:
if overridden or ui_state.status in (UIStatus.DISENGAGED, UIStatus.OVERRIDE):
def _limit_header_color(self, active_side: str, overridden: bool) -> rl.Color:
if self._pedal_override or overridden or ui_state.status in (UIStatus.DISENGAGED, UIStatus.OVERRIDE):
return COLORS.DISENGAGED
if ui_state.status == UIStatus.ENGAGED and active_side in ("slc", "shared"):
return COLORS.ENGAGED
@@ -176,7 +200,7 @@ class UnifiedSpeedWidget(LayoutWidget):
def _draw_active_emphasis(self, rect: rl.Rectangle) -> None:
presentation = self._presentation
if presentation.mode == "merged" or ui_state.status != UIStatus.ENGAGED or presentation.active_side == "none":
if self._pedal_override or presentation.mode == "merged" or ui_state.status != UIStatus.ENGAGED or presentation.active_side == "none":
return
if presentation.mode in ("max_only", "limit_only"):
bounds = rect
@@ -222,6 +246,7 @@ class UnifiedSpeedWidget(LayoutWidget):
def _render(self, rect: rl.Rectangle) -> None:
presentation = self._presentation
state = self._slc_state
speed_color = COLORS.DISENGAGED if self._pedal_override else COLORS.WHITE
rl.draw_rectangle_rounded_lines_ex(
rect, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, 7,
rl.Color(UNIFIED_ACCENT.r, UNIFIED_ACCENT.g, UNIFIED_ACCENT.b, 55),
@@ -240,12 +265,12 @@ class UnifiedSpeedWidget(LayoutWidget):
max_bounds = rl.Rectangle(rect.x, rect.y, rect.width / 2, rect.height) if presentation.mode in ("split", "merged") else rect
limit_bounds = self._speed_limit_bounds(rect)
if self._show_max or presentation.confirmation_pending:
max_color = COLORS.DARK_GREY if not self.hud_renderer.is_cruise_set else COLORS.WHITE
max_color = COLORS.DARK_GREY if not self.hud_renderer.is_cruise_set else speed_color
max_label_color = self._max_header_color(presentation.active_side, self.hud_renderer.is_cruise_set)
self._draw_header(max_bounds, "MAX SET", "speedometer", max_label_color)
if presentation.mode != "merged":
self._draw_centered_text(presentation.max_speed_text, max_bounds, rect.y + 75, VALUE_FONT_SIZE, max_color, bold=True)
self._draw_centered_text(tr(presentation.unit_text), max_bounds, rect.y + 204, UNIT_FONT_SIZE, COLORS.WHITE_TRANSLUCENT)
self._draw_unit(max_bounds, rect.y + 204)
if limit_bounds is not None:
icon_key = source_icon_key(presentation.source)
@@ -253,16 +278,16 @@ class UnifiedSpeedWidget(LayoutWidget):
label_color = self._limit_header_color(presentation.active_side, overridden)
self._draw_header(limit_bounds, "SPEED LIMIT", icon_key, label_color)
if presentation.mode != "merged":
self._draw_centered_text(presentation.posted_speed_text, limit_bounds, rect.y + 75, VALUE_FONT_SIZE, COLORS.WHITE, bold=True)
self._draw_centered_text(presentation.posted_speed_text, limit_bounds, rect.y + 75, VALUE_FONT_SIZE, speed_color, bold=True)
if presentation.confirmation_pending:
self._draw_centered_text(tr("PENDING"), limit_bounds, rect.y + 175, 25, CONFIRMATION_COLOR)
elif presentation.offset_text is not None:
self._draw_offset_pill(limit_bounds, presentation.offset_text, rect.y + 175)
self._draw_centered_text(tr(presentation.unit_text), limit_bounds, rect.y + 204, UNIT_FONT_SIZE, COLORS.WHITE_TRANSLUCENT)
self._draw_unit(limit_bounds, rect.y + 204)
if presentation.mode == "merged":
self._draw_centered_text(presentation.effective_speed_text, rect, rect.y + 98, VALUE_FONT_SIZE, COLORS.WHITE, bold=True)
self._draw_centered_text(tr(presentation.unit_text), rect, rect.y + 204, UNIT_FONT_SIZE, COLORS.WHITE_TRANSLUCENT)
self._draw_centered_text(presentation.effective_speed_text, rect, rect.y + 98, VALUE_FONT_SIZE, speed_color, bold=True)
self._draw_unit(rect, rect.y + 204)
if presentation.offset_text is not None:
self._draw_offset_pill(
limit_bounds, presentation.offset_text, rect.y + MERGED_SEPARATOR_Y - OFFSET_PILL_HEIGHT / 2,
+115 -1
View File
@@ -17,6 +17,7 @@ def make_widget(mode="split", pending=False):
widget._presentation = UnifiedSpeedPresentation(mode, "70", "65", "70", "+5", "mph", "Map Data", pending, "slc")
widget._show_max = True
widget._slc_state = None
widget._pedal_override = False
widget.hud_renderer = SimpleNamespace(is_cruise_set=True)
return widget
@@ -208,7 +209,7 @@ def test_enabled_slc_stays_full_width_when_plan_is_stale(monkeypatch):
widget._snapshot_frame = None
widget.hud_renderer = SimpleNamespace(is_cruise_available=True, is_cruise_set=True, set_speed=70)
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(
sm=SimpleNamespace(frame=1), starpilot_toggles={}, is_metric=False,
sm=SimpleNamespace(frame=1), starpilot_toggles={}, is_metric=False, engaged=False,
))
monkeypatch.setattr(unified_speed, "_is_slc_enabled", lambda: True)
monkeypatch.setattr(unified_speed, "_get_slc_state", lambda: None)
@@ -217,6 +218,119 @@ def test_enabled_slc_stays_full_width_when_plan_is_stale(monkeypatch):
assert widget._presentation.posted_speed_text == "–"
@pytest.fixture
def pedal_snapshot(monkeypatch):
class SubMaster(dict):
pass
sm = SubMaster(carState=SimpleNamespace(gasPressed=True))
sm.frame = 20
sm.valid = {"carState": True}
sm.alive = {"carState": True}
sm.recv_frame = {"carState": 20}
ui = SimpleNamespace(sm=sm, started_frame=10, engaged=True, starpilot_toggles={}, is_metric=False)
widget = make_widget()
widget._snapshot_frame = None
widget.hud_renderer = SimpleNamespace(is_cruise_available=True, is_cruise_set=True, set_speed=70)
monkeypatch.setattr(unified_speed, "ui_state", ui)
monkeypatch.setattr(unified_speed, "_is_slc_enabled", lambda: True)
monkeypatch.setattr(unified_speed, "_get_slc_state", lambda: None)
return widget, ui
@pytest.mark.parametrize("gas,engaged,cruise_set,valid,alive,received,expected", [
(True, True, True, True, True, 20, True),
(False, True, True, True, True, 20, False),
(True, False, True, True, True, 20, False),
(True, True, False, True, True, 20, False),
(True, True, True, False, True, 20, False),
(True, True, True, True, False, 20, False),
(True, True, True, True, True, 9, False),
])
def test_pedal_override_requires_fresh_gas_and_engaged_cruise(pedal_snapshot, gas, engaged, cruise_set, valid, alive, received, expected):
widget, ui = pedal_snapshot
ui.sm["carState"].gasPressed = gas
ui.engaged = engaged
widget.hud_renderer.is_cruise_set = cruise_set
ui.sm.valid["carState"] = valid
ui.sm.alive["carState"] = alive
ui.sm.recv_frame["carState"] = received
widget._refresh_snapshot()
assert widget._pedal_override == expected
def test_pedal_cue_clears_on_release_with_a_persistent_slc_override(pedal_snapshot, monkeypatch):
widget, ui = pedal_snapshot
sm = ui.sm
state = {
"speed_conversion": CV.MS_TO_MPH, "accepted_speed_limit_ms": 65 * CV.MPH_TO_MS,
"effective_target_ms": 70 * CV.MPH_TO_MS, "offset_ms": 5 * CV.MPH_TO_MS,
"speed_limit_changed": False, "unconfirmed_valid": False, "presented_source": "Map Data",
"slc_is_limiting_max_set": False, "slc_overridden_speed": 80 * CV.MPH_TO_MS,
}
monkeypatch.setattr(unified_speed, "_get_slc_state", lambda: state)
widget._refresh_snapshot()
assert widget._pedal_override
presentation = widget._presentation
sm["carState"].gasPressed = False
widget._refresh_snapshot()
assert widget._pedal_override
sm.frame += 1
sm.recv_frame["carState"] = sm.frame
widget._refresh_snapshot()
assert not widget._pedal_override
assert widget._presentation == presentation
assert widget._slc_state["slc_overridden_speed"] > 0
@pytest.mark.parametrize("mode", ["split", "merged", "max_only", "limit_only"])
@pytest.mark.parametrize("unit", ["mph", "km/h"])
def test_pedal_cue_mutes_targets_and_preserves_units_offsets_and_layout(monkeypatch, mode, unit):
widget = make_widget(mode)
widget._pedal_override = True
widget._font_semi_bold = None
widget._show_max = mode != "limit_only"
widget._presentation = replace(widget._presentation, unit_text=unit)
if mode in ("max_only", "limit_only"):
widget._rect.width = 250
values, headers, pauses, offsets, lines = [], [], [], [], []
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.ENGAGED))
monkeypatch.setattr(unified_speed, "draw_control_card", lambda *args, **kwargs: None)
monkeypatch.setattr(unified_speed, "measure_text_cached", lambda *args: rl.Vector2(60, 28))
monkeypatch.setattr(rl, "draw_rectangle_rounded_lines_ex", lambda *args: None)
monkeypatch.setattr(rl, "draw_rectangle_rec", lambda *args: pauses.append(args))
monkeypatch.setattr(rl, "draw_line_ex", lambda *args: lines.append(args))
monkeypatch.setattr(widget, "_draw_merged_separator", lambda *args: None)
monkeypatch.setattr(widget, "_draw_header", lambda bounds, text, icon, color: headers.append(color))
monkeypatch.setattr(widget, "_draw_centered_text", lambda text, bounds, y, size, color, **kwargs: values.append((text, bounds, size, color)))
monkeypatch.setattr(widget, "_draw_offset_pill", lambda bounds, text, y: offsets.append(text))
widget._render(widget.rect)
speed_values = [value for value in values if value[2] == unified_speed.VALUE_FONT_SIZE]
unit_values = [value for value in values if value[2] == unified_speed.UNIT_FONT_SIZE]
expected_speeds = {"split": ["70", "65"], "merged": ["70"], "max_only": ["70"], "limit_only": ["65"]}
assert [value[0] for value in speed_values] == expected_speeds[mode]
assert all(value[3] == unified_speed.COLORS.DISENGAGED for value in speed_values + unit_values)
assert all(color == unified_speed.COLORS.DISENGAGED for color in headers)
assert [value[0] for value in unit_values] == [unit] * (2 if mode == "split" else 1)
assert len(pauses) == 2 * len(unit_values)
assert all(color == unified_speed.OFFSET_COLOR for _bounds, color in pauses)
assert offsets == ([] if mode == "max_only" else ["+5"])
assert not any(line[2] == 3 for line in lines)
for index, value in enumerate(unit_values):
pause = pauses[index * 2][0]
assert pause.x == pytest.approx(value[1].x + (value[1].width - 60) / 2 - 20)
values.clear()
pauses.clear()
widget._pedal_override = False
widget._render(widget.rect)
assert not pauses
assert all(value[3] == unified_speed.COLORS.WHITE for value in values if value[2] == unified_speed.VALUE_FONT_SIZE)
assert all(value[3] == unified_speed.COLORS.WHITE_TRANSLUCENT for value in values if value[2] == unified_speed.UNIT_FONT_SIZE)
def test_split_merged_transitions_keep_the_same_footprint(monkeypatch):
widget = make_widget("split")
monkeypatch.setattr(widget, "_refresh_snapshot", lambda: None)