C4 personality indicator

This commit is contained in:
firestar5683
2026-10-07 21:20:40 -05:00
parent 67b213a5aa
commit ef33866889
8 changed files with 155 additions and 35 deletions
@@ -259,11 +259,12 @@ class AugmentedRoadView(CameraView):
gui_app.measure_frame_phase("model", self._model_renderer.render_with_lead, rect, lead_indicator,
lead_info_mode, lead_info_metric, lateral_active=lateral_active, paint=paint)
def render_model_source_layer(self, rect: rl.Rectangle, *, show_long_indicator: bool = False) -> None:
def render_model_source_layer(self, rect: rl.Rectangle, *, show_long_indicator: bool | None = False) -> None:
self._hud_renderer._update_state()
self._hud_renderer._draw_model_source(rect)
self._long_indicator.set_should_draw(show_long_indicator)
self._long_indicator.render(rect)
if show_long_indicator is not None:
self._long_indicator.set_should_draw(show_long_indicator)
self._long_indicator.render(rect)
def render_stock_confidence_layer(self, rect: rl.Rectangle, *, lateral_active: bool = False) -> None:
self._confidence_ball.render_with_lateral(rect, lateral_active)
@@ -23,6 +23,10 @@ class LongIndicator(Widget):
self._personality: int | None = None
self._personality_changed_time = -HIGHLIGHT_TIME
self._should_draw = False
self._sidebar = False
self._traffic_mode = False
self._sidebar_personality: int | None = None
self._sidebar_long_active: bool | None = None
@staticmethod
def _texture(name: str, width: int, height: int) -> rl.Texture:
@@ -31,16 +35,29 @@ class LongIndicator(Widget):
def set_should_draw(self, should_draw: bool):
self._should_draw = should_draw
def render_sidebar(self, rect: rl.Rectangle, *, personality: int | None = None,
longitudinal_active: bool = False, traffic_mode: bool = False) -> None:
self._sidebar, self._traffic_mode = True, traffic_mode
self._sidebar_personality = personality
self._sidebar_long_active = longitudinal_active if personality is not None else None
try:
self.render(rect)
finally:
self._sidebar, self._traffic_mode = False, False
self._sidebar_personality = self._sidebar_long_active = None
def _render(self, rect: rl.Rectangle) -> None:
sm = ui_state.sm
if sm.recv_frame['selfdriveState'] < ui_state.started_frame or not sm['selfdriveState'].enabled or not ui_state.has_longitudinal_control:
if self._sidebar_personality is None and (sm.recv_frame['selfdriveState'] < ui_state.started_frame or
not sm['selfdriveState'].enabled or not ui_state.has_longitudinal_control):
self._personality = None
self._personality_changed_time = -HIGHLIGHT_TIME
self._alpha_filter.x = 0.0
return
# hidden under alerts and set speed
visible = self._should_draw and sm['selfdriveState'].alertSize == log.SelfdriveState.AlertSize.none
visible = self._should_draw and (self._sidebar_personality is not None or
sm['selfdriveState'].alertSize == log.SelfdriveState.AlertSize.none)
alpha = self._alpha_filter.update(visible)
self._draw_lead_car(rect, alpha)
self._draw_distance_bars(rect, alpha, visible)
@@ -48,10 +65,12 @@ class LongIndicator(Widget):
def _draw_lead_car(self, rect: rl.Rectangle, alpha: float) -> None:
sm = ui_state.sm
plan = sm['longitudinalPlan']
has_lead = sm.alive['longitudinalPlan'] and plan.hasLead
has_lead = self._sidebar_long_active is not False and sm.alive['longitudinalPlan'] and plan.hasLead
e2e = has_lead and plan.longitudinalPlanSource == log.LongitudinalPlan.LongitudinalPlanSource.e2e
white_f, green_f = self._lead_car_filters
if self._sidebar_long_active is False:
white_f.x, green_f.x = 0.35, 0.0
white_alpha = white_f.update(0.0 if e2e else 0.9 if has_lead else 0.35)
green_alpha = green_f.update(float(e2e))
@@ -62,7 +81,7 @@ class LongIndicator(Widget):
def _draw_distance_bars(self, rect: rl.Rectangle, alpha: float, visible: bool) -> None:
sm = ui_state.sm
now = rl.get_time()
personality = sm['selfdriveState'].personality.raw
personality = self._sidebar_personality if self._sidebar_personality is not None else sm['selfdriveState'].personality.raw
if self._personality is not None and personality != self._personality:
self._personality_changed_time = now
self._personality = personality
@@ -72,18 +91,23 @@ class LongIndicator(Widget):
highlight = now - self._personality_changed_time < HIGHLIGHT_TIME
# blink at double the turn signal rate (2.67 Hz) while overriding the gas
overriding = any(e.name == log.OnroadEvent.EventName.gasPressedOverride for e in sm['onroadEvents'])
overriding = self._sidebar_long_active is not False and any(
e.name == log.OnroadEvent.EventName.gasPressedOverride for e in sm['onroadEvents'])
blink = 0.35 / 0.9 if overriding and now % 0.375 > 0.1875 else 1.0
count = personality + 1
count = 1 if self._traffic_mode else personality + 1
for i, ((white, green), y, (active_f, green_f)) in enumerate(zip(self._txt_distance, (122, 136, 152), self._distance_filters, strict=True)):
active = active_f.update(float(i < count))
green_alpha = green_f.update(float(highlight and i == count - 1))
green_alpha = green_f.update(float(highlight and i == count - 1 and not self._traffic_mode))
# only lit bars blink on override
self._draw_centered(white, rect, y, (0.35 * (1 - active) + 0.9 * (active - green_alpha) * blink) * alpha)
color = (200, 32, 48) if self._traffic_mode and i < count else (255, 255, 255)
self._draw_centered(white, rect, y, (0.35 * (1 - active) + 0.9 * (active - green_alpha) * blink) * alpha, color)
self._draw_centered(green, rect, y, green_alpha * blink * alpha)
@staticmethod
def _draw_centered(texture: rl.Texture, rect: rl.Rectangle, y: float, alpha: float) -> None:
pos = rl.Vector2(rect.x + 46 - texture.width / 2, rect.y + y - texture.height / 2)
rl.draw_texture_ex(texture, pos, 0.0, 1.0, rl.Color(255, 255, 255, round(255 * alpha)))
def _draw_centered(self, texture: rl.Texture, rect: rl.Rectangle, y: float, alpha: float,
color: tuple[int, int, int] = (255, 255, 255)) -> None:
scale = min((rect.width - 4) / 76, (rect.height - 4) / 94) if self._sidebar else 1.0
center_x = rect.x + rect.width / 2 if self._sidebar else rect.x + 46
center_y = rect.y + rect.height / 2 + (y - 126) * scale if self._sidebar else rect.y + y
pos = rl.Vector2(center_x - texture.width * scale / 2, center_y - texture.height * scale / 2)
rl.draw_texture_ex(texture, pos, 0.0, scale, rl.Color(*color, round(255 * alpha)))
+1
View File
@@ -354,6 +354,7 @@ class OnroadView:
rail = rl.Rectangle(0, 0, 536, 240)
if use_stock:
stock_layer(rail, state)
self.compact_sidebar.render_personality(rail, state)
else:
self.compact_sidebar.render(rail, state)
clip.begin_scissor_mode(0, 0, 476, 240)
@@ -226,6 +226,7 @@ class MiciSidebarWidgets:
def __init__(self, fonts: BitmapFonts | None = None):
self.fonts = fonts
self.personality_renderer = None
self._confidence_filter = FirstOrderFilter(-0.5, 0.5, 1 / 60)
self._confidence_drive_frame: int | None = None
self._confidence_stamp_ns: int | None = None
@@ -233,14 +234,22 @@ class MiciSidebarWidgets:
def render(self, rect: rl.Rectangle, state: OnroadState) -> None:
sidebar = rl.Rectangle(rect.x + rect.width - 60, rect.y, 60, rect.height)
rl.draw_rectangle(int(sidebar.x), int(sidebar.y), int(sidebar.width), int(sidebar.height), rl.BLACK)
for key, render in (("model_confidence", self._confidence_ball), ("conditional_mode", self._conditional),
("following_distance", self._personality)):
for key, render in (("model_confidence", self._confidence_ball), ("conditional_mode", self._conditional)):
position = placement(state.customization, "compact", key)
if not position["enabled"] or (position["x"] < sidebar.x and state.alert.size != AlertSize.NONE):
continue
bounds = rl.Rectangle(position["x"], position["y"], 60, 80)
draw_widget_frame(bounds, state.customization, "compact", key)
render(bounds, state)
self.render_personality(rect, state)
def render_personality(self, rect: rl.Rectangle, state: OnroadState) -> None:
position = placement(state.customization, "compact", "following_distance")
if not position["enabled"] or (position["x"] < rect.x + rect.width - 60 and state.alert.size != AlertSize.NONE):
return
bounds = rl.Rectangle(position["x"], position["y"], 60, 80)
draw_widget_frame(bounds, state.customization, "compact", "following_distance")
self._personality(bounds, state)
def _conditional(self, center: rl.Rectangle, state: OnroadState) -> None:
conditional = conditional_status(state)
@@ -349,16 +358,21 @@ class MiciSidebarWidgets:
def _personality(self, rect: rl.Rectangle, state: OnroadState) -> None:
cx, cy = rect.x + rect.width / 2, rect.y + rect.height / 2
bottom_y, lane_top_y = cy + 24, cy - 24
color = rl.Color(*rgba(state.customization, "text", "compact", "following_distance"))
_line(cx - 25, bottom_y, cx - 13, lane_top_y, color, 3)
_line(cx + 25, bottom_y, cx + 13, lane_top_y, color, 3)
accent = rl.Color(200, 32, 48, 255) if state.traffic_mode else rl.Color(112, 192, 216, 255)
stack_count = 1 if state.traffic_mode else state.personality + 1
for y, half_width in ((cy + 12, 18), (cy - 1, 14), (cy - 14, 10))[:stack_count]:
_line(cx - half_width, y, cx + half_width, y, accent, 5)
_line(cx - half_width, y, cx - half_width - 3, y + 7, accent, 4)
_line(cx + half_width, y, cx + half_width + 3, y + 7, accent, 4)
if self.personality_renderer is not None:
label = state.traffic_display is not None and state.traffic_display.state in ('active', 'paused', 'off')
bounds = rl.Rectangle(rect.x, rect.y, rect.width, rect.height - 14 if label else rect.height)
self.personality_renderer(bounds, state)
else:
bottom_y, lane_top_y = cy + 24, cy - 24
color = rl.Color(*rgba(state.customization, "text", "compact", "following_distance"))
_line(cx - 25, bottom_y, cx - 13, lane_top_y, color, 3)
_line(cx + 25, bottom_y, cx + 13, lane_top_y, color, 3)
stack_count = 1 if state.traffic_mode else state.personality + 1
for y, half_width in ((cy + 12, 18), (cy - 1, 14), (cy - 14, 10))[:stack_count]:
_line(cx - half_width, y, cx + half_width, y, accent, 5)
_line(cx - half_width, y, cx - half_width - 3, y + 7, accent, 4)
_line(cx + half_width, y, cx + half_width + 3, y + 7, accent, 4)
if state.traffic_display is not None and self.fonts is not None and state.alert.size != AlertSize.FULL:
short = {'active': 'TRF', 'paused': 'PAUSE', 'off': 'OFF'}.get(state.traffic_display.state)
if short is None:
+8 -4
View File
@@ -177,10 +177,8 @@ class StarShellSession:
if profile == Profile.COMPACT:
self.view.onroad.stock_confidence_layer = lambda rect, state: render_stock_confidence(camera_owner, rect, state)
self.view.onroad.stock_confidence_reset = camera_owner.reset_stock_confidence_layer
self.view.onroad.extra_overlays = lambda rect, state: camera_owner.render_model_source_layer(
rect, show_long_indicator=state.alert.size == AlertSize.NONE and
state.appearance.camera_view != CameraViewChoice.DRIVER and not state.reverse_driver_camera and
self.view.onroad.compact_hud._set_speed_alpha.x <= 1e-2)
self.view.onroad.extra_overlays = lambda rect, state: camera_owner.render_model_source_layer(rect, show_long_indicator=None)
self.view.onroad.compact_sidebar.personality_renderer = self._render_personality
except Exception:
self.fonts.close()
raise
@@ -292,6 +290,12 @@ class StarShellSession:
fresh=monitor is not None and driver is not None,
onroad=ui_state.is_onroad(), top_icons=top_icons)
def _render_personality(self, rect, state):
indicator = self.camera_owner._long_indicator
indicator.set_should_draw(state.alert.size == AlertSize.NONE)
indicator.render_sidebar(rect, personality=state.personality,
longitudinal_active=state.longitudinal_active, traffic_mode=state.traffic_mode)
def _favorite_actions(self):
actions = mapped_actions(lambda page: self.feature_snapshot(page, favorite=True), self.feature_request,
lambda: self.appearance_owner.snapshot(self.profile), self.appearance_owner.apply)
@@ -21,14 +21,19 @@ def test_native_chestnut_status_accepts_typed_params(raw, expected):
assert state.chestnut_active is expected
def test_custom_model_source_layer_uses_native_status_animation():
@pytest.mark.parametrize('indicator', [False, None])
def test_custom_model_source_layer_uses_native_status_animation(indicator):
view = Mock(spec=AugmentedRoadView, _hud_renderer=Mock(), _long_indicator=Mock())
rect = rl.Rectangle(0, 0, 476, 240)
AugmentedRoadView.render_model_source_layer(view, rect)
AugmentedRoadView.render_model_source_layer(view, rect, show_long_indicator=indicator)
view._hud_renderer._update_state.assert_called_once_with()
view._hud_renderer._draw_model_source.assert_called_once_with(rect)
view._long_indicator.set_should_draw.assert_called_once_with(False)
view._long_indicator.render.assert_called_once_with(rect)
if indicator is None:
view._long_indicator.set_should_draw.assert_not_called()
view._long_indicator.render.assert_not_called()
else:
view._long_indicator.set_should_draw.assert_called_once_with(False)
view._long_indicator.render.assert_called_once_with(rect)
@pytest.mark.parametrize('compiled', [False, True])
@@ -184,7 +184,7 @@ def make_session(monkeypatch, profile, camera):
monkeypatch.setattr(runtime_app, 'ui_state', NS(params=object(), CP=None, sm={}))
monkeypatch.delenv('SP_ONROAD_VISUAL_PREVIEW', raising=False)
def view(*_, camera_layer, **__):
return NS(camera_layer=camera_layer, onroad=NS(unified_speed=NS(source_bounds=Mock())))
return NS(camera_layer=camera_layer, onroad=NS(unified_speed=NS(source_bounds=Mock()), compact_sidebar=NS()))
monkeypatch.setattr(runtime_app, 'ShellView', view)
return runtime_app.StarShellSession(profile, camera)
@@ -51,3 +51,74 @@ def test_indicator_tracks_lead_policy_and_yields_to_alerts(display):
for _ in range(60):
widget._render(rect)
assert all(call.args[3] < .001 for call in widget._draw_centered.call_args_list[-8:])
def test_stock_indicator_fits_bottom_right_and_moved_personality_slot(display, monkeypatch):
widget, _ = display
widget._draw_centered = indicator.LongIndicator._draw_centered.__get__(widget)
monkeypatch.setattr(widget, 'render', widget._render)
widget._txt_lead_car = (NS(width=35, height=27), NS(width=50, height=42))
widget._txt_distance = [(NS(width=w, height=h), NS(width=gw, height=gh))
for w, h, gw, gh in ((32, 7, 60, 35), (40, 9, 68, 37), (48, 11, 76, 39))]
draw = Mock()
monkeypatch.setattr(rl, 'draw_texture_ex', draw)
for rect in (rl.Rectangle(476, 160, 60, 80), rl.Rectangle(476, 160, 60, 66), rl.Rectangle(300, 20, 60, 80)):
draw.reset_mock()
widget.render_sidebar(rect)
assert draw.call_count == 8
for call in draw.call_args_list:
texture, pos, _, scale, _ = call.args
assert rect.x <= pos.x and pos.x + texture.width * scale <= rect.x + rect.width
assert rect.y <= pos.y and pos.y + texture.height * scale <= rect.y + rect.height
assert not widget._sidebar
def test_stock_sidebar_preserves_traffic_accent_then_restores_personality(display, monkeypatch):
widget, _ = display
monkeypatch.setattr(widget, 'render', widget._render)
rect = rl.Rectangle(476, 160, 60, 80)
widget.render_sidebar(rect, traffic_mode=True)
first_bar = widget._draw_centered.call_args_list[-6]
assert first_bar.args[4] == (200, 32, 48)
for _ in range(60):
widget.render_sidebar(rect)
bars = widget._draw_centered.call_args_list[-6::2]
assert bars[1].args[3] > bars[2].args[3]
assert all(call.args[4] == (255, 255, 255) for call in bars)
@pytest.mark.parametrize('missing', ['longitudinal', 'engagement', 'drive'])
def test_sidebar_keeps_selected_personality_without_claiming_active_control(display, monkeypatch, missing):
widget, state = display
if missing == 'longitudinal':
state.has_longitudinal_control = False
elif missing == 'engagement':
state.sm['selfdriveState'].enabled = False
else:
state.started_frame = 13
monkeypatch.setattr(widget, 'render', widget._render)
for _ in range(60):
widget.render_sidebar(rl.Rectangle(476, 160, 60, 80), personality=0)
draws = widget._draw_centered.call_args_list[-8:]
assert draws[0].args[3] == pytest.approx(.35, abs=.001)
assert draws[1].args[3] < .001
assert draws[2].args[3] > .89
assert draws[4].args[3] == pytest.approx(.35, abs=.001)
assert draws[6].args[3] == pytest.approx(.35, abs=.001)
def test_sidebar_disengagement_clears_active_lead_and_gas_override_immediately(display, monkeypatch):
widget, state = display
monkeypatch.setattr(widget, 'render', widget._render)
monkeypatch.setattr(indicator.rl, 'get_time', lambda: 1.1)
rect = rl.Rectangle(476, 160, 60, 80)
for _ in range(60):
widget.render_sidebar(rect, personality=1, longitudinal_active=True)
assert widget._draw_centered.call_args_list[-7].args[3] > .95
state.sm['onroadEvents'] = [NS(name=log.OnroadEvent.EventName.gasPressedOverride)]
widget.render_sidebar(rect, personality=1, longitudinal_active=False)
draws = widget._draw_centered.call_args_list[-8:]
assert draws[0].args[3] == pytest.approx(.35, abs=.001)
assert draws[1].args[3] == 0.0
assert draws[2].args[3] > .89
assert draws[4].args[3] > .89