Drive native replay turn arrows and clarify independent simulation resets

This commit is contained in:
firestar5683
2026-09-20 09:40:28 -07:00
parent a0b239ff38
commit 1a3f4bde4e
7 changed files with 49 additions and 7 deletions
+10 -1
View File
@@ -4,8 +4,17 @@ from pathlib import Path
from openpilot.selfdrive.ui.ui_state import UIState, device
from preparing_awake import PreparationWake
from replay_display_hold import ReplayDisplayHold
from replay_ui_controls import ReplayUIControls, ReplayStateView, isolated_replay
from replay_ui_controls import ReplayUIControls, ReplayStateView, isolated_replay, apply_turn_intent
replay_controls=ReplayUIControls(os.environ['ROADSCORE_STATUS_FILE'],enabled=isolated_replay(os.environ))
if replay_controls.enabled:
from openpilot.selfdrive.ui.mici.onroad.hud_renderer import TurnIntent
from openpilot.selfdrive.ui.ui_state import ui_state
original_turn_intent=TurnIntent._update_state
def replay_turn_intent(self):
sm=ui_state.sm
if isinstance(sm,ReplayStateView) and apply_turn_intent(self,sm.signal_mode):return
return original_turn_intent(self)
TurnIntent._update_state=replay_turn_intent
display_hold=ReplayDisplayHold(os.environ.get("ROADSCORE_AUDIO_DRAIN_FILE"), enabled=os.environ.get("OPENPILOT_PREFIX")=="roadscore_replay")
original_state=UIState._update_state
def replay_state(self,*args,**kwargs):
+2 -1
View File
@@ -41,7 +41,8 @@ def gesture_view(state):
if signal in ('left', 'right', 'off'):
shaker = state.get('signal_shaker') or {}
description = 'Shaker' if shaker.get('rendered_active') else 'Replay signal' if signal != 'off' else 'Recorded signals muted'
return f'Simulated {signal} / {description}', 'active', 'simulated_signal'
title = f'Simulated {signal}' + (' shaker' if shaker.get('rendered_active') else '')
return f'{title} / {description}', 'active', 'simulated_signal'
engagement = state.get('engagement_presentation') or {}
if engagement.get('enabled') and not engagement.get('simulated') and engagement.get('input_fresh') and engagement.get('rendered_state') == 'transition':
return ('Engaged / Opening music' if engagement.get('active') else 'Disengaged / Contained music'), 'active', 'engagement'
+20
View File
@@ -15,6 +15,26 @@ def isolated_replay(environ):
and environ.get('OPENPILOT_PREFIX') == 'roadscore_replay')
def apply_turn_intent(widget, signal_mode):
"""Use the existing native arrow textures/animation without inventing events."""
if signal_mode not in ('left', 'right', 'off'):
return False
direction = -1 if signal_mode == 'left' else 1 if signal_mode == 'right' else 0
if direction:
if not widget._pre or widget._turn_intent_direction != direction:
widget._turn_intent_rotation_filter.x = -direction * widget.FADE_IN_ANGLE
widget._pre = True
widget._turn_intent_direction = direction
widget._turn_intent_alpha_filter.update(1)
widget._turn_intent_rotation_filter.update(0)
else:
widget._pre = False
widget._turn_intent_direction = 0
widget._turn_intent_alpha_filter.update(0)
widget._turn_intent_rotation_filter.update(0)
return True
class ReplayUIControls:
def __init__(self, status_path, *, enabled=False, clock=time.monotonic):
self.path = Path(status_path)
+13 -1
View File
@@ -6,7 +6,7 @@ from types import SimpleNamespace
from cereal import log, car, custom
from cue_timing import REFERENCE
from replay_ui_controls import ReplayUIControls, ReplayStateView, isolated_replay
from replay_ui_controls import ReplayUIControls, ReplayStateView, isolated_replay, apply_turn_intent
class Subscriber:
@@ -104,5 +104,17 @@ class Tests(unittest.TestCase):
state=delay.apply(snapshot,dict(sequence=1,callback_wall=99.5,dac_wall=99.8,cues=cues))
self.path.write_text(json.dumps(state));self.view.update()
self.assertEqual((self.view.mode,self.view.signal_mode),('disengaged','right'))
def test_native_turn_arrow_direction_and_recorded_fallback(self):
class Filter:
x=0
def update(self,target):self.target=target
widget=SimpleNamespace(_pre=False,_turn_intent_direction=0,FADE_IN_ANGLE=30,
_turn_intent_alpha_filter=Filter(),_turn_intent_rotation_filter=Filter())
self.assertTrue(apply_turn_intent(widget,'left'))
self.assertEqual(widget._turn_intent_direction,-1);self.assertEqual(widget._turn_intent_alpha_filter.target,1)
self.assertTrue(apply_turn_intent(widget,'right'));self.assertEqual(widget._turn_intent_direction,1)
self.assertTrue(apply_turn_intent(widget,'off'));self.assertEqual(widget._turn_intent_direction,0)
self.assertEqual(widget._turn_intent_alpha_filter.target,0)
self.assertFalse(apply_turn_intent(widget,'recorded'))
if __name__=='__main__':unittest.main()
@@ -36,7 +36,7 @@
</div>
<!-- Snackbar for messages -->
<div id="snackbar_wrapper"></div>
<script type="module" src="/assets/mobile/js/app.js?v=replay-controls-2"></script>
<script type="module" src="/assets/mobile/js/app.js?v=replay-controls-3"></script>
</body>
</html>
@@ -1,4 +1,4 @@
import { RoadScore } from "./views/RoadScore.js?v=replay-controls-2"
import { RoadScore } from "./views/RoadScore.js?v=replay-controls-3"
import { createApp, h } from "vue"
import { AppShell } from "./components/AppShell.js?v=roadscore-calibration-2"
import { Home } from "./views/Home.js"
@@ -137,11 +137,11 @@ export const RoadScore = {
</div></GalaxySection>
<GalaxySection title="Replay simulation" icon="bi-play-circle" :collapsible="false"><div style="padding:16px">
<p class="gx-row__desc">Replay simulation changes displayed engagement, turn signals and music. It does not engage or control the vehicle.</p>
<p><strong>{{status.demo?.available ? (status.demo.mode === 'engaged' ? 'Simulated engage' : status.demo.mode === 'disengaged' ? 'Simulated disengage' : 'Using recorded state') : 'Replay unavailable'}}</strong></p>
<p><strong>{{status.demo?.available ? (status.demo.mode === 'engaged' ? 'Simulated engage' : status.demo.mode === 'disengaged' ? 'Simulated disengage' : 'Using recorded engagement') : 'Replay unavailable'}}</strong></p>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="gx-btn" :disabled="busy || !status.demo?.available" @click="action('demo_engagement', {session_id:status.demo.session_id, mode:'engaged'})">Simulate engage</button>
<button class="gx-btn" :disabled="busy || !status.demo?.available" @click="action('demo_engagement', {session_id:status.demo.session_id, mode:'disengaged'})">Simulate disengage</button>
<button class="gx-btn" :disabled="busy || !status.demo?.available" @click="action('demo_engagement', {session_id:status.demo.session_id, mode:'recorded'})">Use recorded state</button>
<button class="gx-btn" :disabled="busy || !status.demo?.available" @click="action('demo_engagement', {session_id:status.demo.session_id, mode:'recorded'})">Recorded engagement</button>
</div>
<p><strong>{{status.demo?.signal_mode === 'recorded' ? 'Using recorded signals' : 'Simulated signals: ' + (status.demo?.signal_mode || 'recorded')}}</strong></p>
<div style="display:flex;gap:8px;flex-wrap:wrap">