mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-01 03:43:46 +08:00
UI Pass
This commit is contained in:
@@ -75,8 +75,21 @@ def _speed_limit_pulse_color(base: rl.Color, alpha: int) -> rl.Color:
|
||||
|
||||
# ── State ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _is_slc_enabled() -> bool:
|
||||
toggles = getattr(ui_state, "starpilot_toggles", {})
|
||||
if "speed_limit_controller" in toggles:
|
||||
return bool(toggles["speed_limit_controller"])
|
||||
return ui_state.ui_params.get_bool("SpeedLimitController")
|
||||
|
||||
|
||||
def _get_slc_state():
|
||||
"""Extract SLC state from SubMaster. Returns dict or None if stale/hidden."""
|
||||
slc_enabled = _is_slc_enabled()
|
||||
params = ui_state.ui_params
|
||||
if not (slc_enabled or params.get_bool("ShowSpeedLimits")):
|
||||
_pulse.clear()
|
||||
return None
|
||||
|
||||
sm = ui_state.sm
|
||||
if sm.recv_frame["starpilotPlan"] < ui_state.started_frame:
|
||||
_pulse.clear()
|
||||
@@ -84,15 +97,10 @@ def _get_slc_state():
|
||||
|
||||
plan = sm["starpilotPlan"]
|
||||
speed_limit_changed = plan.speedLimitChanged
|
||||
presented_source = getattr(plan, 'slcPresentedSpeedLimitSource', '')
|
||||
|
||||
params = ui_state.ui_params
|
||||
show_slc = params.get_bool("ShowSpeedLimits")
|
||||
unconfirmed_valid = plan.unconfirmedSlcSpeedLimit > 1
|
||||
|
||||
if not show_slc:
|
||||
_pulse.clear()
|
||||
return None
|
||||
|
||||
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
dashboard_sl = sm["starpilotCarState"].dashboardSpeedLimit if sm.valid.get("starpilotCarState", False) else 0.0
|
||||
vision_enabled = params.get_bool("VisionSpeedLimitDetection")
|
||||
@@ -105,9 +113,6 @@ def _get_slc_state():
|
||||
|
||||
# The pulse uses the accepted raw limit, so unit changes cannot retrigger it.
|
||||
_tick_pulse(plan.slcSpeedLimitSource, plan.slcSpeedLimit)
|
||||
toggles = getattr(ui_state, 'starpilot_toggles', {})
|
||||
slc_enabled = toggles.get('speed_limit_controller') if 'speed_limit_controller' in toggles else params.get_bool('SpeedLimitController')
|
||||
|
||||
return {
|
||||
'accepted_speed_limit_ms': plan.slcSpeedLimit,
|
||||
# Match the control target's non-negative base before cluster compensation.
|
||||
@@ -115,9 +120,11 @@ def _get_slc_state():
|
||||
'offset_ms': plan.slcSpeedLimitOffset,
|
||||
'slc_overridden_speed': plan.slcOverriddenSpeed,
|
||||
'speed_limit_source': plan.slcSpeedLimitSource,
|
||||
'presented_source': getattr(plan, 'slcPresentedSpeedLimitSource', plan.slcSpeedLimitSource),
|
||||
'slc_enabled': bool(slc_enabled),
|
||||
'slc_is_limiting_max_set': bool(getattr(plan, 'slcIsLimitingMaxSet', False)),
|
||||
# Older publishers/replays decode the new Text field as "", rather than omitting the attribute.
|
||||
'presented_source': presented_source or plan.slcSpeedLimitSource,
|
||||
'slc_enabled': slc_enabled,
|
||||
# Both UI fields were added together; older plans have no published limiting state.
|
||||
'slc_is_limiting_max_set': bool(getattr(plan, 'slcIsLimitingMaxSet', False)) if presented_source else None,
|
||||
'unconfirmed_speed_limit': max(0.0, plan.unconfirmedSlcSpeedLimit * speed_conversion),
|
||||
'unconfirmed_valid': unconfirmed_valid,
|
||||
'speed_limit_changed': speed_limit_changed,
|
||||
@@ -251,10 +258,19 @@ def _draw_source_icon(icon_key: str, x: float, y: float, size: float, color: rl.
|
||||
)
|
||||
rl.draw_circle_v(pin_center, size * 0.09, _SOURCE_PANEL_BG)
|
||||
elif icon_key == "dashboard":
|
||||
dashboard_scale = 1.22
|
||||
# The Dashboard speed-limit source is a vehicle glyph, distinct from Max Set's gauge.
|
||||
body = rl.Rectangle(x + size * 0.10, y + size * 0.43, size * 0.80, size * 0.29)
|
||||
rl.draw_rectangle_rounded_lines_ex(body, 0.30, 8, stroke, color)
|
||||
rl.draw_line_ex(rl.Vector2(x + size * 0.25, body.y), rl.Vector2(x + size * 0.36, y + size * 0.27), stroke, color)
|
||||
rl.draw_line_ex(rl.Vector2(x + size * 0.36, y + size * 0.27), rl.Vector2(x + size * 0.68, y + size * 0.27), stroke, color)
|
||||
rl.draw_line_ex(rl.Vector2(x + size * 0.68, y + size * 0.27), rl.Vector2(x + size * 0.79, body.y), stroke, color)
|
||||
for wheel_x in (x + size * 0.27, x + size * 0.73):
|
||||
rl.draw_circle_v(rl.Vector2(wheel_x, y + size * 0.75), size * 0.07, color)
|
||||
elif icon_key == "speedometer":
|
||||
gauge_scale = 1.22
|
||||
pivot = rl.Vector2(cx, cy + size * 0.17)
|
||||
inner_radius = size * 0.27 * dashboard_scale
|
||||
outer_radius = size * 0.34 * dashboard_scale
|
||||
inner_radius = size * 0.27 * gauge_scale
|
||||
outer_radius = size * 0.34 * gauge_scale
|
||||
ring_segments = max(24, int(size * 0.25))
|
||||
rl.draw_ring(pivot, inner_radius, outer_radius, 190, 350, ring_segments, color)
|
||||
cap_radius = (outer_radius - inner_radius) / 2
|
||||
@@ -279,7 +295,7 @@ def _draw_source_icon(icon_key: str, x: float, y: float, size: float, color: rl.
|
||||
stroke,
|
||||
color,
|
||||
)
|
||||
rl.draw_circle_v(pivot, max(2.0, size * 0.06 * dashboard_scale), color)
|
||||
rl.draw_circle_v(pivot, max(2.0, size * 0.06 * gauge_scale), color)
|
||||
|
||||
|
||||
def _draw_sources_bubble_empty_state(panel_rect: rl.Rectangle) -> None:
|
||||
|
||||
@@ -17,30 +17,44 @@ class UnifiedSpeedPresentation:
|
||||
|
||||
|
||||
def resolve_unified_speed(show_max: bool, cruise_set: bool, max_speed: float,
|
||||
slc_state: dict | None, is_metric: bool) -> UnifiedSpeedPresentation:
|
||||
slc_state: dict | None, slc_enabled: bool, is_metric: bool) -> UnifiedSpeedPresentation:
|
||||
"""Compare the rounded values the driver sees; ignore override speed for layout."""
|
||||
unit = "km/h" if is_metric else "mph"
|
||||
max_text = str(round(max_speed)) if cruise_set else "–"
|
||||
if slc_state is None:
|
||||
return UnifiedSpeedPresentation("max_only", max_text, "–", "–", None, unit, "None", False, "max" if cruise_set else "none")
|
||||
posted_text = effective_text = "–"
|
||||
offset_text = None
|
||||
source = "None"
|
||||
pending = has_limit = slc_is_limiting = False
|
||||
if slc_state is not None:
|
||||
conversion = slc_state['speed_conversion']
|
||||
accepted = slc_state['accepted_speed_limit_ms']
|
||||
pending = bool(slc_state['speed_limit_changed'] and slc_state['unconfirmed_valid'])
|
||||
source = slc_state['presented_source']
|
||||
has_limit = (source not in ("", "None") and accepted > 1) or pending
|
||||
if has_limit:
|
||||
posted_text = str(round(slc_state['unconfirmed_speed_limit'])) if pending else str(round(accepted * conversion))
|
||||
effective = slc_state['effective_target_ms']
|
||||
slc_is_limiting = slc_state['slc_is_limiting_max_set']
|
||||
if slc_is_limiting is None:
|
||||
slc_is_limiting = cruise_set and accepted > 1 and 0 < effective * conversion < max_speed
|
||||
effective_text = str(round(effective * conversion)) if effective > 0 else "–"
|
||||
offset_display = round(slc_state['offset_ms'] * conversion)
|
||||
offset_text = f"{offset_display:+d}" if offset_display else None
|
||||
else:
|
||||
source = "None"
|
||||
|
||||
conversion = slc_state['speed_conversion']
|
||||
accepted = slc_state['accepted_speed_limit_ms']
|
||||
effective = slc_state['effective_target_ms']
|
||||
candidate = slc_state['unconfirmed_speed_limit']
|
||||
pending = bool(slc_state['speed_limit_changed'] and slc_state['unconfirmed_valid'])
|
||||
source = slc_state['presented_source']
|
||||
has_limit = (source not in ("", "None") and accepted > 1) or pending
|
||||
if not has_limit:
|
||||
return UnifiedSpeedPresentation("max_only", max_text, "–", "–", None, unit, "None", False, "max" if cruise_set else "none")
|
||||
# Max-only is valid only when SLC is disabled.
|
||||
if pending:
|
||||
mode = "split"
|
||||
elif slc_enabled:
|
||||
mode = "merged" if show_max and cruise_set and has_limit and max_text == effective_text else "split" if show_max else "limit_only"
|
||||
elif has_limit:
|
||||
mode = "split" if show_max else "limit_only"
|
||||
else:
|
||||
mode = "max_only"
|
||||
|
||||
posted_text = str(round(candidate)) if pending else str(round(accepted * conversion))
|
||||
effective_text = str(round(effective * conversion)) if effective > 0 else "–"
|
||||
offset_display = round(slc_state['offset_ms'] * conversion)
|
||||
offset_text = f"{offset_display:+d}" if offset_display else None
|
||||
merged = bool(show_max and cruise_set and slc_state['slc_enabled'] and not pending and max_text == effective_text)
|
||||
mode = "split" if pending else "merged" if merged else "split" if show_max else "limit_only"
|
||||
active_side = "none" if slc_state['slc_overridden_speed'] else "shared" if merged else (
|
||||
"slc" if slc_state['slc_is_limiting_max_set'] else "max" if (show_max or pending) and cruise_set else "none"
|
||||
active_side = "none" if slc_state is not None and slc_state['slc_overridden_speed'] else "shared" if mode == "merged" else (
|
||||
"slc" if slc_enabled and cruise_set and slc_is_limiting else
|
||||
"max" if (show_max or pending) and cruise_set else "none"
|
||||
)
|
||||
return UnifiedSpeedPresentation(mode, max_text, posted_text, effective_text, offset_text, unit, source, pending, active_side)
|
||||
|
||||
@@ -9,14 +9,13 @@ from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.onroad.hud_renderer import COLORS
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.slc_speed_limit import (
|
||||
_draw_source_icon, _draw_sources_bubble, _get_slc_state, _speed_limit_pulse_color, source_icon_key,
|
||||
_draw_source_icon, _draw_sources_bubble, _get_slc_state, _is_slc_enabled, _speed_limit_pulse_color, source_icon_key,
|
||||
)
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.unified_speed_presentation import (
|
||||
UnifiedSpeedPresentation, resolve_unified_speed,
|
||||
)
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.widget_style import (
|
||||
CONTROL_BG, CONTROL_BORDER, CONTROL_BORDER_WIDTH, CONTROL_ROUNDNESS,
|
||||
CONTROL_SEGMENTS, draw_control_card, roundness_for,
|
||||
CONTROL_BG, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, draw_control_card, roundness_for,
|
||||
)
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets.base import LayoutWidget
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
@@ -34,6 +33,7 @@ UNIT_FONT_SIZE = 28
|
||||
OFFSET_FONT_SIZE = 25
|
||||
CONFIRMATION_COLOR = rl.Color(188, 132, 255, 255)
|
||||
OFFSET_COLOR = rl.Color(188, 156, 226, 220)
|
||||
UNIFIED_ACCENT = rl.Color(160, 96, 230, 230)
|
||||
|
||||
|
||||
class UnifiedSpeedWidget(LayoutWidget):
|
||||
@@ -45,6 +45,7 @@ class UnifiedSpeedWidget(LayoutWidget):
|
||||
self._font_semi_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._font_bold = gui_app.font(FontWeight.BOLD)
|
||||
self._slc_state: dict | None = None
|
||||
self._slc_enabled = False
|
||||
self._presentation: UnifiedSpeedPresentation | None = None
|
||||
self._show_max = False
|
||||
self._snapshot_frame: int | None = None
|
||||
@@ -54,6 +55,7 @@ class UnifiedSpeedWidget(LayoutWidget):
|
||||
if frame is not None and frame == self._snapshot_frame:
|
||||
return
|
||||
self._snapshot_frame = frame
|
||||
self._slc_enabled = _is_slc_enabled()
|
||||
self._slc_state = _get_slc_state()
|
||||
self._show_max = (
|
||||
self.hud_renderer.is_cruise_available and
|
||||
@@ -61,7 +63,7 @@ class UnifiedSpeedWidget(LayoutWidget):
|
||||
)
|
||||
self._presentation = resolve_unified_speed(
|
||||
self._show_max, self.hud_renderer.is_cruise_set, self.hud_renderer.set_speed,
|
||||
self._slc_state, ui_state.is_metric,
|
||||
self._slc_state, self._slc_enabled, ui_state.is_metric,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -99,23 +101,26 @@ class UnifiedSpeedWidget(LayoutWidget):
|
||||
|
||||
def _draw_header(self, bounds: rl.Rectangle, text: str, icon_key: str | None, label_color: rl.Color) -> None:
|
||||
text = tr(text)
|
||||
text_size = measure_text_cached(self._font_semi_bold, text, HEADER_FONT_SIZE)
|
||||
slot_gap = 9
|
||||
group_width = HEADER_ICON_SIZE + slot_gap + text_size.x
|
||||
font_size = HEADER_FONT_SIZE
|
||||
icon_width = HEADER_ICON_SIZE + 9 if icon_key else 0
|
||||
while font_size > 16 and measure_text_cached(self._font_semi_bold, text, font_size).x + icon_width > bounds.width - 24:
|
||||
font_size -= 1
|
||||
text_size = measure_text_cached(self._font_semi_bold, text, font_size)
|
||||
group_width = icon_width + text_size.x
|
||||
group_x = bounds.x + (bounds.width - group_width) / 2
|
||||
icon_y = bounds.y + 20
|
||||
if icon_key:
|
||||
_draw_source_icon(icon_key, group_x, icon_y, HEADER_ICON_SIZE, rl.WHITE)
|
||||
rl.draw_text_ex(
|
||||
self._font_semi_bold, text,
|
||||
rl.Vector2(group_x + HEADER_ICON_SIZE + slot_gap, icon_y + (HEADER_ICON_SIZE - text_size.y) / 2),
|
||||
HEADER_FONT_SIZE, 0, label_color,
|
||||
rl.Vector2(group_x + icon_width, icon_y + (HEADER_ICON_SIZE - text_size.y) / 2),
|
||||
font_size, 0, label_color,
|
||||
)
|
||||
|
||||
def _draw_offset_pill(self, bounds: rl.Rectangle, text: str, y: float) -> None:
|
||||
text_size = measure_text_cached(self._font_semi_bold, text, OFFSET_FONT_SIZE)
|
||||
width = max(56.0, text_size.x + 20.0)
|
||||
pill = rl.Rectangle(bounds.x + (bounds.width - width) / 2, y, width, 34)
|
||||
pill = rl.Rectangle(bounds.x + (bounds.width - width) / 2, y, width, 30)
|
||||
rl.draw_rectangle_rounded(pill, roundness_for(pill, 17), 8, rl.Color(32, 20, 45, 170))
|
||||
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)
|
||||
@@ -138,7 +143,7 @@ class UnifiedSpeedWidget(LayoutWidget):
|
||||
|
||||
def _draw_active_emphasis(self, rect: rl.Rectangle) -> None:
|
||||
presentation = self._presentation
|
||||
if ui_state.status != UIStatus.ENGAGED or presentation.active_side == "none":
|
||||
if 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
|
||||
@@ -151,29 +156,52 @@ class UnifiedSpeedWidget(LayoutWidget):
|
||||
rl.draw_line_ex(
|
||||
rl.Vector2(bounds.x + 18, rect.y + 65),
|
||||
rl.Vector2(bounds.x + bounds.width - 18, rect.y + 65),
|
||||
3, COLORS.ENGAGED,
|
||||
3, UNIFIED_ACCENT,
|
||||
)
|
||||
|
||||
def _draw_merged_separator(self, rect: rl.Rectangle) -> None:
|
||||
center = rect.x + rect.width / 2
|
||||
shelf_y = rect.y + 76
|
||||
valley_y = shelf_y + 12
|
||||
color = rl.Color(UNIFIED_ACCENT.r, UNIFIED_ACCENT.g, UNIFIED_ACCENT.b, 170)
|
||||
rl.draw_line_ex(rl.Vector2(rect.x + 18, shelf_y), rl.Vector2(center - 34, shelf_y), 2, color)
|
||||
rl.draw_spline_segment_bezier_cubic(
|
||||
rl.Vector2(center - 34, shelf_y), rl.Vector2(center - 19, shelf_y),
|
||||
rl.Vector2(center - 23, valley_y), rl.Vector2(center - 7, valley_y), 2, color,
|
||||
)
|
||||
rl.draw_line_ex(rl.Vector2(center - 7, valley_y), rl.Vector2(center + 7, valley_y), 2, color)
|
||||
rl.draw_spline_segment_bezier_cubic(
|
||||
rl.Vector2(center + 7, valley_y), rl.Vector2(center + 23, valley_y),
|
||||
rl.Vector2(center + 19, shelf_y), rl.Vector2(center + 34, shelf_y), 2, color,
|
||||
)
|
||||
rl.draw_line_ex(rl.Vector2(center + 34, shelf_y), rl.Vector2(rect.x + rect.width - 18, shelf_y), 2, color)
|
||||
|
||||
def _draw_speed_limit_border(self, rect: rl.Rectangle, right: rl.Rectangle, color: rl.Color) -> None:
|
||||
# Clip the shared rounded outline so only the Speed Limit side changes.
|
||||
rl.begin_scissor_mode(int(right.x), int(rect.y), int(right.width + 1), int(rect.height + 1))
|
||||
try:
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, CONTROL_BORDER_WIDTH, color)
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, 3, color)
|
||||
finally:
|
||||
rl.end_scissor_mode()
|
||||
if right.x > rect.x:
|
||||
if self._presentation.mode == "split":
|
||||
rl.draw_line_ex(rl.Vector2(right.x, rect.y + 8), rl.Vector2(right.x, rect.y + rect.height - 8), 3, color)
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
presentation = self._presentation
|
||||
state = self._slc_state
|
||||
draw_control_card(rect, fill=CONTROL_BG, border=CONTROL_BORDER)
|
||||
rl.draw_rectangle_rounded_lines_ex(
|
||||
rect, CONTROL_ROUNDNESS, CONTROL_SEGMENTS, 7,
|
||||
rl.Color(UNIFIED_ACCENT.r, UNIFIED_ACCENT.g, UNIFIED_ACCENT.b, 55),
|
||||
)
|
||||
draw_control_card(rect, fill=CONTROL_BG, border=UNIFIED_ACCENT, border_width=2)
|
||||
if presentation.mode == "split":
|
||||
divider_x = rect.x + rect.width / 2
|
||||
rl.draw_line_ex(
|
||||
rl.Vector2(divider_x, rect.y + 8), rl.Vector2(divider_x, rect.y + rect.height - 8),
|
||||
2, CONTROL_BORDER,
|
||||
2, rl.Color(UNIFIED_ACCENT.r, UNIFIED_ACCENT.g, UNIFIED_ACCENT.b, 110),
|
||||
)
|
||||
elif presentation.mode == "merged":
|
||||
self._draw_merged_separator(rect)
|
||||
|
||||
self._draw_active_emphasis(rect)
|
||||
max_bounds = rl.Rectangle(rect.x, rect.y, rect.width / 2, rect.height) if presentation.mode in ("split", "merged") else rect
|
||||
@@ -181,10 +209,10 @@ class UnifiedSpeedWidget(LayoutWidget):
|
||||
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_label_color = self._max_header_color(presentation.active_side, self.hud_renderer.is_cruise_set)
|
||||
self._draw_header(max_bounds, "MAX SET", "dashboard", max_label_color)
|
||||
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 + 79, VALUE_FONT_SIZE, max_color, bold=True)
|
||||
self._draw_centered_text(tr(presentation.unit_text), max_bounds, rect.y + 210, UNIT_FONT_SIZE, COLORS.WHITE_TRANSLUCENT)
|
||||
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)
|
||||
|
||||
if limit_bounds is not None:
|
||||
icon_key = source_icon_key(presentation.source)
|
||||
@@ -192,18 +220,18 @@ 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 + 79, VALUE_FONT_SIZE, COLORS.WHITE, bold=True)
|
||||
self._draw_centered_text(presentation.posted_speed_text, limit_bounds, rect.y + 75, VALUE_FONT_SIZE, COLORS.WHITE, 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 + 171)
|
||||
self._draw_centered_text(tr(presentation.unit_text), limit_bounds, rect.y + 210, UNIT_FONT_SIZE, COLORS.WHITE_TRANSLUCENT)
|
||||
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)
|
||||
|
||||
if presentation.mode == "merged":
|
||||
self._draw_centered_text(presentation.effective_speed_text, rect, rect.y + 84, VALUE_FONT_SIZE, COLORS.WHITE, bold=True)
|
||||
self._draw_centered_text(tr(presentation.unit_text), rect, rect.y + 210, UNIT_FONT_SIZE, COLORS.WHITE_TRANSLUCENT)
|
||||
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)
|
||||
if presentation.offset_text is not None:
|
||||
self._draw_offset_pill(limit_bounds, presentation.offset_text, rect.y + 70)
|
||||
self._draw_offset_pill(limit_bounds, presentation.offset_text, rect.y + 69)
|
||||
|
||||
if presentation.confirmation_pending and limit_bounds is not None:
|
||||
intensity = (1.0 + math.sin(2.0 * math.pi * rl.get_time())) / 2.0
|
||||
@@ -212,8 +240,8 @@ class UnifiedSpeedWidget(LayoutWidget):
|
||||
self._draw_speed_limit_border(rect, limit_bounds, pulse)
|
||||
else:
|
||||
if limit_bounds is not None and state is not None:
|
||||
vision_color = _speed_limit_pulse_color(CONTROL_BORDER, CONTROL_BORDER.a)
|
||||
if (vision_color.r, vision_color.g, vision_color.b) != (CONTROL_BORDER.r, CONTROL_BORDER.g, CONTROL_BORDER.b):
|
||||
vision_color = _speed_limit_pulse_color(UNIFIED_ACCENT, UNIFIED_ACCENT.a)
|
||||
if (vision_color.r, vision_color.g, vision_color.b) != (UNIFIED_ACCENT.r, UNIFIED_ACCENT.g, UNIFIED_ACCENT.b):
|
||||
self._draw_speed_limit_border(rect, limit_bounds, vision_color)
|
||||
if state is not None and ui_state.ui_params.get_bool("SpeedLimitSources"):
|
||||
_draw_sources_bubble(state, rect)
|
||||
|
||||
@@ -26,10 +26,11 @@ def slc_state(posted_mph=65, offset_mph=0, source="Map Data", *, pending_mph=0,
|
||||
(80, 65, 5, "split"),
|
||||
(70, 70, 0, "merged"),
|
||||
(70, 65, 5, "merged"),
|
||||
(70, 65, 4, "split"),
|
||||
(65, 70, -5, "merged"),
|
||||
])
|
||||
def test_split_and_merge_use_effective_accepted_limit(max_speed, posted, offset, expected_mode):
|
||||
result = resolve_unified_speed(True, True, max_speed, slc_state(posted, offset), False)
|
||||
result = resolve_unified_speed(True, True, max_speed, slc_state(posted, offset), True, False)
|
||||
assert result.mode == expected_mode
|
||||
assert result.posted_speed_text == str(posted)
|
||||
assert result.effective_speed_text == str(posted + offset)
|
||||
@@ -38,7 +39,7 @@ def test_split_and_merge_use_effective_accepted_limit(max_speed, posted, offset,
|
||||
|
||||
def test_pending_candidate_forces_split_without_replacing_accepted_target():
|
||||
state = slc_state(65, 5, source="Vision", pending_mph=75)
|
||||
result = resolve_unified_speed(True, True, 70, state, False)
|
||||
result = resolve_unified_speed(True, True, 70, state, True, False)
|
||||
assert result.mode == "split"
|
||||
assert result.confirmation_pending
|
||||
assert result.posted_speed_text == "75"
|
||||
@@ -48,67 +49,90 @@ def test_pending_candidate_forces_split_without_replacing_accepted_target():
|
||||
|
||||
def test_resolution_after_confirmation_uses_same_equality_rule():
|
||||
state = slc_state(65, 5, pending_mph=75)
|
||||
assert resolve_unified_speed(True, True, 70, state, False).mode == "split"
|
||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "split"
|
||||
state["speed_limit_changed"] = state["unconfirmed_valid"] = False
|
||||
assert resolve_unified_speed(True, True, 70, state, False).mode == "merged"
|
||||
assert resolve_unified_speed(True, True, 80, state, False).mode == "split"
|
||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
||||
assert resolve_unified_speed(True, True, 80, state, True, False).mode == "split"
|
||||
|
||||
|
||||
def test_source_change_and_override_do_not_change_layout():
|
||||
state = slc_state(65, 5)
|
||||
assert resolve_unified_speed(True, True, 70, state, False).mode == "merged"
|
||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
||||
state["presented_source"] = "Vision"
|
||||
result = resolve_unified_speed(True, True, 70, state, True, False)
|
||||
assert result.mode == "merged"
|
||||
assert result.source == "Vision"
|
||||
state["presented_source"] = "Dashboard"
|
||||
state["slc_overridden_speed"] = 40.0
|
||||
result = resolve_unified_speed(True, True, 70, state, False)
|
||||
result = resolve_unified_speed(True, True, 70, state, True, False)
|
||||
assert result.mode == "merged"
|
||||
assert result.source == "Dashboard"
|
||||
|
||||
|
||||
def test_source_target_change_recomputes_layout_independently():
|
||||
state = slc_state(65, 5, source="Map Data")
|
||||
assert resolve_unified_speed(True, True, 70, state, False).mode == "merged"
|
||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
||||
state.update(slc_state(55, 5, source="Vision"))
|
||||
result = resolve_unified_speed(True, True, 70, state, False)
|
||||
result = resolve_unified_speed(True, True, 70, state, True, False)
|
||||
assert result.mode == "split"
|
||||
assert result.source == "Vision"
|
||||
|
||||
|
||||
def test_active_side_uses_published_control_semantic():
|
||||
state = slc_state(65, 5, limiting=True)
|
||||
assert resolve_unified_speed(True, True, 80, state, False).active_side == "slc"
|
||||
assert resolve_unified_speed(True, True, 80, state, True, False).active_side == "slc"
|
||||
state["slc_is_limiting_max_set"] = False
|
||||
assert resolve_unified_speed(True, True, 80, state, False).active_side == "max"
|
||||
assert resolve_unified_speed(True, True, 80, state, True, False).active_side == "max"
|
||||
state["slc_overridden_speed"] = 40.0
|
||||
assert resolve_unified_speed(True, True, 80, state, False).active_side == "none"
|
||||
assert resolve_unified_speed(True, True, 80, state, True, False).active_side == "none"
|
||||
|
||||
|
||||
def test_display_only_speed_limit_stays_split():
|
||||
result = resolve_unified_speed(True, True, 70, slc_state(70, enabled=False), False)
|
||||
result = resolve_unified_speed(True, True, 70, slc_state(70, enabled=False), False, False)
|
||||
assert result.mode == "split"
|
||||
|
||||
|
||||
def test_disabled_confirmation_does_not_force_split():
|
||||
state = slc_state(65, 5, pending_mph=75)
|
||||
state["speed_limit_changed"] = False
|
||||
assert resolve_unified_speed(True, True, 70, state, False).mode == "merged"
|
||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
||||
|
||||
|
||||
def test_missing_limit_never_renders_zero_or_a_stale_source():
|
||||
result = resolve_unified_speed(True, True, 70, slc_state(0, source="None"), False)
|
||||
assert result.mode == "max_only"
|
||||
result = resolve_unified_speed(True, True, 70, slc_state(0, source="None"), True, False)
|
||||
assert result.mode == "split"
|
||||
assert result.posted_speed_text == "–"
|
||||
assert result.effective_speed_text == "–"
|
||||
assert result.source == "None"
|
||||
|
||||
|
||||
def test_missing_limit_with_slc_disabled_allows_max_only():
|
||||
result = resolve_unified_speed(True, True, 70, slc_state(0, source="None", enabled=False), False, False)
|
||||
assert result.mode == "max_only"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("show_max,expected_mode", [(True, "split"), (False, "limit_only")])
|
||||
def test_stale_slc_data_keeps_speed_limit_region(show_max, expected_mode):
|
||||
result = resolve_unified_speed(show_max, True, 70, None, True, False)
|
||||
assert result.mode == expected_mode
|
||||
assert result.posted_speed_text == "–"
|
||||
assert result.effective_speed_text == "–"
|
||||
assert result.source == "None"
|
||||
|
||||
|
||||
def test_missing_data_with_slc_disabled_uses_max_only():
|
||||
assert resolve_unified_speed(True, True, 70, None, False, False).mode == "max_only"
|
||||
|
||||
|
||||
def test_persisted_previous_limit_without_source_remains_visible():
|
||||
result = resolve_unified_speed(True, True, 70, slc_state(45, source="Previous Limit"), False)
|
||||
result = resolve_unified_speed(True, True, 70, slc_state(45, source="Previous Limit"), True, False)
|
||||
assert result.mode == "split"
|
||||
assert result.posted_speed_text == "45"
|
||||
assert result.source == "Previous Limit"
|
||||
|
||||
|
||||
def test_low_limit_with_large_negative_offset_preserves_configured_offset():
|
||||
result = resolve_unified_speed(True, True, 70, slc_state(5, -99), False)
|
||||
result = resolve_unified_speed(True, True, 70, slc_state(5, -99), True, False)
|
||||
assert result.mode == "split"
|
||||
assert result.posted_speed_text == "5"
|
||||
assert result.effective_speed_text == "–"
|
||||
@@ -117,7 +141,7 @@ def test_low_limit_with_large_negative_offset_preserves_configured_offset():
|
||||
|
||||
def test_metric_and_rounding_follow_the_displayed_value():
|
||||
state = slc_state(65.4, 4.4, metric=True)
|
||||
result = resolve_unified_speed(True, True, 70, state, True)
|
||||
result = resolve_unified_speed(True, True, 70, state, True, True)
|
||||
assert result.mode == "merged"
|
||||
assert result.posted_speed_text == "65"
|
||||
assert result.offset_text == "+4"
|
||||
@@ -126,17 +150,37 @@ def test_metric_and_rounding_follow_the_displayed_value():
|
||||
|
||||
def test_invisible_fraction_does_not_keep_card_split():
|
||||
state = slc_state(65.1, 5.2)
|
||||
result = resolve_unified_speed(True, True, 70.4, state, False)
|
||||
result = resolve_unified_speed(True, True, 70.4, state, True, False)
|
||||
assert result.mode == "merged"
|
||||
|
||||
|
||||
def test_hidden_max_still_shows_posted_limit():
|
||||
result = resolve_unified_speed(False, True, 70, slc_state(65), False)
|
||||
result = resolve_unified_speed(False, True, 70, slc_state(65), True, False)
|
||||
assert result.mode == "limit_only"
|
||||
result = resolve_unified_speed(False, True, 70, slc_state(65, enabled=False), False, False)
|
||||
assert result.mode == "limit_only"
|
||||
|
||||
|
||||
def test_confirmation_forces_split_even_when_max_is_hidden():
|
||||
result = resolve_unified_speed(False, True, 70, slc_state(65, pending_mph=75), False)
|
||||
result = resolve_unified_speed(False, True, 70, slc_state(65, pending_mph=75), True, False)
|
||||
assert result.mode == "split"
|
||||
assert result.max_speed_text == "70"
|
||||
assert result.confirmation_pending
|
||||
|
||||
|
||||
def test_offset_max_pending_and_source_transitions_recompute_mode():
|
||||
state = slc_state(65)
|
||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "split"
|
||||
state.update(slc_state(65, 5))
|
||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
||||
assert resolve_unified_speed(True, True, 75, state, True, False).mode == "split"
|
||||
state.update(slc_state(65, 5, pending_mph=75))
|
||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "split"
|
||||
state.update(slc_state(65, 5))
|
||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
||||
state.update(slc_state(0, source="None"))
|
||||
missing = resolve_unified_speed(True, True, 70, state, True, False)
|
||||
assert missing.mode == "split"
|
||||
assert missing.posted_speed_text == "–"
|
||||
state.update(slc_state(65, 5))
|
||||
assert resolve_unified_speed(True, True, 70, state, True, False).mode == "merged"
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
from types import SimpleNamespace
|
||||
from dataclasses import replace
|
||||
|
||||
import pyray as rl
|
||||
import pytest
|
||||
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.unified_speed_presentation import UnifiedSpeedPresentation
|
||||
from cereal import custom
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui.onroad.starpilot import slc_speed_limit
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.unified_speed_presentation import UnifiedSpeedPresentation, resolve_unified_speed
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.widgets import unified_speed
|
||||
|
||||
|
||||
@@ -32,6 +37,17 @@ def test_confirmation_touch_only_accepts_on_speed_limit_side(monkeypatch):
|
||||
assert writes == [("SpeedLimitAccepted", True)]
|
||||
|
||||
|
||||
def test_merged_speed_limit_side_toggles_sources(monkeypatch):
|
||||
widget = make_widget("merged")
|
||||
writes = []
|
||||
params = SimpleNamespace(get_bool=lambda _key: False, put_bool=lambda key, value: writes.append((key, value)))
|
||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(ui_params=params))
|
||||
widget._handle_mouse_press(rl.Vector2(100, 150))
|
||||
assert writes == []
|
||||
widget._handle_mouse_press(rl.Vector2(400, 150))
|
||||
assert writes == [("SpeedLimitSources", True)]
|
||||
|
||||
|
||||
def test_diagnostic_sources_can_be_dismissed_from_max_only_card(monkeypatch):
|
||||
widget = make_widget("max_only")
|
||||
widget._slc_state = {}
|
||||
@@ -43,37 +59,240 @@ def test_diagnostic_sources_can_be_dismissed_from_max_only_card(monkeypatch):
|
||||
|
||||
|
||||
def test_right_border_overlay_is_clipped_to_speed_limit_side(monkeypatch):
|
||||
widget = make_widget()
|
||||
events = []
|
||||
monkeypatch.setattr(unified_speed.rl, "begin_scissor_mode", lambda *args: events.append(("begin", args)))
|
||||
monkeypatch.setattr(unified_speed.rl, "draw_rectangle_rounded_lines_ex", lambda *args: events.append(("outline", args)))
|
||||
monkeypatch.setattr(unified_speed.rl, "draw_line_ex", lambda *args: events.append(("divider", args)))
|
||||
monkeypatch.setattr(unified_speed.rl, "end_scissor_mode", lambda: events.append(("end",)))
|
||||
rect = widget.rect
|
||||
right = widget._speed_limit_bounds(rect)
|
||||
widget._draw_speed_limit_border(rect, right, rl.Color(188, 132, 255, 200))
|
||||
assert events[0] == ("begin", (290, 75, 261, 251))
|
||||
assert [event[0] for event in events] == ["begin", "outline", "end", "divider"]
|
||||
for mode, expected in (("split", ["begin", "outline", "end", "divider"]),
|
||||
("merged", ["begin", "outline", "end"])):
|
||||
events.clear()
|
||||
widget = make_widget(mode)
|
||||
rect = widget.rect
|
||||
right = widget._speed_limit_bounds(rect)
|
||||
widget._draw_speed_limit_border(rect, right, rl.Color(188, 132, 255, 200))
|
||||
assert events[0] == ("begin", (290, 75, 261, 251))
|
||||
assert [event[0] for event in events] == expected
|
||||
|
||||
|
||||
def test_split_and_merged_draw_one_card_with_both_headers(monkeypatch):
|
||||
cards = []
|
||||
lines = []
|
||||
monkeypatch.setattr(unified_speed, "draw_control_card", lambda *args, **kwargs: cards.append(args[0]))
|
||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.DISENGAGED,
|
||||
ui_params=SimpleNamespace(get_bool=lambda _key: False)))
|
||||
monkeypatch.setattr(unified_speed.rl, "draw_line_ex", lambda *args: None)
|
||||
monkeypatch.setattr(unified_speed.rl, "draw_line_ex", lambda *args: lines.append(args))
|
||||
monkeypatch.setattr(unified_speed.rl, "draw_rectangle_rounded_lines_ex", lambda *args: None)
|
||||
for mode in ("split", "merged"):
|
||||
lines.clear()
|
||||
widget = make_widget(mode)
|
||||
headers = []
|
||||
values = []
|
||||
separators = []
|
||||
offsets = []
|
||||
monkeypatch.setattr(widget, "_draw_header", lambda _bounds, text, icon, _color, rows=headers: rows.append((text, icon)))
|
||||
monkeypatch.setattr(widget, "_draw_centered_text", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(widget, "_draw_offset_pill", lambda *args: None)
|
||||
monkeypatch.setattr(widget, "_draw_centered_text", lambda text, *args, rows=values, **kwargs: rows.append(text))
|
||||
monkeypatch.setattr(widget, "_draw_offset_pill", lambda bounds, text, y, rows=offsets: rows.append((bounds, text, y)))
|
||||
monkeypatch.setattr(widget, "_draw_merged_separator", lambda _rect, rows=separators: rows.append(True))
|
||||
monkeypatch.setattr(widget, "_draw_active_emphasis", lambda *args: None)
|
||||
widget._render(widget.rect)
|
||||
assert headers == [("MAX SET", "dashboard"), ("SPEED LIMIT", "map")]
|
||||
assert headers == [("MAX SET", "speedometer"), ("SPEED LIMIT", "map")]
|
||||
assert separators == ([True] if mode == "merged" else [])
|
||||
assert sum(line[0].x == line[1].x == 290 for line in lines) == (1 if mode == "split" else 0)
|
||||
assert values == (["70", "mph"] if mode == "merged" else ["70", "mph", "65", "mph"])
|
||||
assert offsets[0][0].x == 290
|
||||
assert offsets[0][2] == (144 if mode == "merged" else 250)
|
||||
assert len(cards) == 2
|
||||
|
||||
|
||||
def test_merged_draws_effective_speed_once_and_skips_active_line(monkeypatch):
|
||||
widget = make_widget("merged")
|
||||
widget._presentation = replace(widget._presentation, max_speed_text="71", effective_speed_text="70", active_side="shared")
|
||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.ENGAGED,
|
||||
ui_params=SimpleNamespace(get_bool=lambda _key: False)))
|
||||
monkeypatch.setattr(unified_speed, "draw_control_card", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(unified_speed.rl, "draw_rectangle_rounded_lines_ex", lambda *args: None)
|
||||
lines = []
|
||||
monkeypatch.setattr(unified_speed.rl, "draw_line_ex", lambda *args: lines.append(args))
|
||||
monkeypatch.setattr(widget, "_draw_merged_separator", lambda _rect: None)
|
||||
monkeypatch.setattr(widget, "_draw_header", lambda *args: None)
|
||||
monkeypatch.setattr(widget, "_draw_offset_pill", lambda *args: None)
|
||||
values = []
|
||||
monkeypatch.setattr(widget, "_draw_centered_text", lambda text, *args, **kwargs: values.append(text))
|
||||
widget._render(widget.rect)
|
||||
assert values == ["70", "mph"]
|
||||
assert lines == []
|
||||
|
||||
|
||||
def test_merged_separator_has_shallow_center_dip(monkeypatch):
|
||||
widget = make_widget("merged")
|
||||
segments = []
|
||||
monkeypatch.setattr(unified_speed.rl, "draw_line_ex", lambda *args: segments.append(("line", args)))
|
||||
monkeypatch.setattr(unified_speed.rl, "draw_spline_segment_bezier_cubic", lambda *args: segments.append(("curve", args)))
|
||||
widget._draw_merged_separator(widget.rect)
|
||||
assert [segment[0] for segment in segments] == ["line", "curve", "line", "curve", "line"]
|
||||
assert segments[0][1][0].y == widget.rect.y + 76
|
||||
assert segments[2][1][0].y == widget.rect.y + 88
|
||||
|
||||
|
||||
def test_enabled_slc_stays_full_width_when_plan_is_stale(monkeypatch):
|
||||
widget = make_widget("split")
|
||||
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,
|
||||
))
|
||||
monkeypatch.setattr(unified_speed, "_is_slc_enabled", lambda: True)
|
||||
monkeypatch.setattr(unified_speed, "_get_slc_state", lambda: None)
|
||||
assert widget.get_size() == (520.0, 250.0)
|
||||
assert widget.is_visible
|
||||
assert widget._presentation.posted_speed_text == "–"
|
||||
|
||||
|
||||
def test_split_merged_transitions_keep_the_same_footprint(monkeypatch):
|
||||
widget = make_widget("split")
|
||||
monkeypatch.setattr(widget, "_refresh_snapshot", lambda: None)
|
||||
sizes = []
|
||||
for mode in ("split", "merged", "split", "merged"):
|
||||
widget._presentation = replace(widget._presentation, mode=mode)
|
||||
sizes.append(widget.get_size())
|
||||
assert sizes == [(520.0, 250.0)] * 4
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def slc_ui(monkeypatch):
|
||||
class Params(dict):
|
||||
def get_bool(self, key):
|
||||
return bool(self.get(key))
|
||||
|
||||
def get(self, key, encoding=None):
|
||||
return super().get(key)
|
||||
|
||||
class SubMaster(dict):
|
||||
recv_frame = {"starpilotPlan": 10}
|
||||
valid = {"starpilotCarState": True}
|
||||
|
||||
plan = custom.StarPilotPlan.new_message(
|
||||
slcSpeedLimit=30 * CV.MPH_TO_MS, slcSpeedLimitOffset=0.0, slcSpeedLimitSource="Map Data",
|
||||
slcOverriddenSpeed=0.0, slcMapSpeedLimit=30 * CV.MPH_TO_MS, slcMapboxSpeedLimit=0.0,
|
||||
slcNextSpeedLimit=0.0, unconfirmedSlcSpeedLimit=0.0, speedLimitChanged=False,
|
||||
)
|
||||
sm = SubMaster(starpilotPlan=plan, starpilotCarState=SimpleNamespace(dashboardSpeedLimit=0.0))
|
||||
sm.recv_frame = sm.recv_frame.copy()
|
||||
params = Params(SpeedLimitController=True, ShowSpeedLimits=False)
|
||||
ui = SimpleNamespace(
|
||||
sm=sm, started_frame=10, is_metric=False, ui_params=params, starpilot_toggles={},
|
||||
params_memory=SimpleNamespace(get_float=lambda _key: 0.0),
|
||||
)
|
||||
monkeypatch.setattr(slc_speed_limit, "ui_state", ui)
|
||||
monkeypatch.setattr(slc_speed_limit, "starpilot_state", SimpleNamespace(car_state=SimpleNamespace(hasDashSpeedLimits=True)))
|
||||
monkeypatch.setattr(slc_speed_limit, "_tick_pulse", lambda *args: None)
|
||||
return ui
|
||||
|
||||
|
||||
def test_slc_state_extraction_respects_feature_and_display_toggles(slc_ui):
|
||||
assert slc_speed_limit._is_slc_enabled()
|
||||
assert slc_speed_limit._get_slc_state()["slc_enabled"]
|
||||
slc_ui.starpilot_toggles["speed_limit_controller"] = False
|
||||
assert not slc_speed_limit._is_slc_enabled()
|
||||
assert slc_speed_limit._get_slc_state() is None
|
||||
slc_ui.ui_params["ShowSpeedLimits"] = True
|
||||
assert not slc_speed_limit._get_slc_state()["slc_enabled"]
|
||||
slc_ui.starpilot_toggles["speed_limit_controller"] = True
|
||||
slc_ui.sm.recv_frame["starpilotPlan"] = 9
|
||||
assert slc_speed_limit._get_slc_state() is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("presented_source,expected_source,expected_speed", [
|
||||
("", "Map Data", "30"),
|
||||
("Map Data", "Map Data", "30"),
|
||||
("None", "None", "–"),
|
||||
("Previous Limit", "Previous Limit", "30"),
|
||||
("Vision", "Vision", "30"),
|
||||
])
|
||||
def test_serialized_plan_source_defaults_and_explicit_values(slc_ui, presented_source, expected_source, expected_speed):
|
||||
message = slc_ui.sm["starpilotPlan"]
|
||||
if presented_source:
|
||||
message.slcPresentedSpeedLimitSource = presented_source
|
||||
# Replay decodes older plans with a present but empty Text attribute.
|
||||
with custom.StarPilotPlan.from_bytes(message.to_bytes()) as plan:
|
||||
slc_ui.sm["starpilotPlan"] = plan
|
||||
state = slc_speed_limit._get_slc_state()
|
||||
result = resolve_unified_speed(True, True, 35, state, True, False)
|
||||
assert result.source == expected_source
|
||||
assert result.posted_speed_text == expected_speed
|
||||
assert result.mode == "split"
|
||||
|
||||
|
||||
def test_legacy_replay_limit_and_offset_merge_with_max_set(slc_ui):
|
||||
message = slc_ui.sm["starpilotPlan"]
|
||||
message.slcSpeedLimitOffset = 5 * CV.MPH_TO_MS
|
||||
with custom.StarPilotPlan.from_bytes(message.to_bytes()) as plan:
|
||||
slc_ui.sm["starpilotPlan"] = plan
|
||||
result = resolve_unified_speed(True, True, 35, slc_speed_limit._get_slc_state(), True, False)
|
||||
assert (result.source, result.posted_speed_text, result.effective_speed_text) == ("Map Data", "30", "35")
|
||||
assert (result.mode, result.offset_text) == ("merged", "+5")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("presented_source,limiting,max_speed,enabled,overridden,expected_side,line_x", [
|
||||
("", False, 40, True, False, "slc", 308),
|
||||
("", False, 34, True, False, "max", 48),
|
||||
("", False, 35, True, False, "shared", None),
|
||||
("", False, 40, False, False, "max", 48),
|
||||
("", False, 40, True, True, "none", None),
|
||||
("Map Data", False, 40, True, False, "max", 48),
|
||||
("Map Data", True, 40, True, False, "slc", 308),
|
||||
])
|
||||
def test_active_underline_with_legacy_and_current_plans(slc_ui, monkeypatch, presented_source, limiting,
|
||||
max_speed, enabled, overridden, expected_side, line_x):
|
||||
message = slc_ui.sm["starpilotPlan"]
|
||||
message.slcSpeedLimitOffset = 5 * CV.MPH_TO_MS
|
||||
message.slcPresentedSpeedLimitSource = presented_source
|
||||
message.slcIsLimitingMaxSet = limiting
|
||||
message.slcOverriddenSpeed = 40 * CV.MPH_TO_MS if overridden else 0.0
|
||||
slc_ui.starpilot_toggles["speed_limit_controller"] = enabled
|
||||
slc_ui.ui_params["ShowSpeedLimits"] = True
|
||||
with custom.StarPilotPlan.from_bytes(message.to_bytes()) as plan:
|
||||
slc_ui.sm["starpilotPlan"] = plan
|
||||
presentation = resolve_unified_speed(True, True, max_speed, slc_speed_limit._get_slc_state(), enabled, False)
|
||||
assert presentation.active_side == expected_side
|
||||
|
||||
widget = make_widget(presentation.mode)
|
||||
widget._presentation = presentation
|
||||
monkeypatch.setattr(unified_speed, "ui_state", SimpleNamespace(status=unified_speed.UIStatus.ENGAGED))
|
||||
lines = []
|
||||
monkeypatch.setattr(unified_speed.rl, "draw_line_ex", lambda *args: lines.append(args))
|
||||
widget._draw_active_emphasis(widget.rect)
|
||||
if line_x is None:
|
||||
assert lines == []
|
||||
else:
|
||||
assert len(lines) == 1
|
||||
assert (lines[0][0].x, lines[0][0].y, lines[0][1].x) == (line_x, 140, line_x + 224)
|
||||
assert lines[0][3] == unified_speed.UNIFIED_ACCENT
|
||||
|
||||
|
||||
def test_legacy_plan_without_active_source_does_not_use_diagnostic_map_limit(slc_ui):
|
||||
message = slc_ui.sm["starpilotPlan"]
|
||||
message.slcSpeedLimitSource = "None"
|
||||
with custom.StarPilotPlan.from_bytes(message.to_bytes()) as plan:
|
||||
slc_ui.sm["starpilotPlan"] = plan
|
||||
state = slc_speed_limit._get_slc_state()
|
||||
assert round(state["map_sl"]) == 30
|
||||
result = resolve_unified_speed(True, True, 35, state, True, False)
|
||||
assert (result.source, result.posted_speed_text, result.mode) == ("None", "–", "split")
|
||||
|
||||
|
||||
def test_legacy_pending_candidate_remains_visible_without_active_source(slc_ui):
|
||||
message = slc_ui.sm["starpilotPlan"]
|
||||
message.slcSpeedLimitSource = "None"
|
||||
message.unconfirmedSlcSpeedLimit = 45 * CV.MPH_TO_MS
|
||||
message.speedLimitChanged = True
|
||||
with custom.StarPilotPlan.from_bytes(message.to_bytes()) as plan:
|
||||
slc_ui.sm["starpilotPlan"] = plan
|
||||
result = resolve_unified_speed(True, True, 35, slc_speed_limit._get_slc_state(), True, False)
|
||||
assert (result.posted_speed_text, result.mode, result.confirmation_pending) == ("45", "split", True)
|
||||
|
||||
|
||||
def test_header_colors_preserve_engaged_disengaged_and_override_semantics(monkeypatch):
|
||||
widget = make_widget()
|
||||
colors = unified_speed.COLORS
|
||||
|
||||
Reference in New Issue
Block a user