mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-10 14:33:42 +08:00
C4 personality indicator
This commit is contained in:
@@ -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)))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user