Keep native replay display visible through audio drain

This commit is contained in:
firestar5683
2026-09-19 20:14:13 -07:00
parent cf3042bd1c
commit c7863cc1d0
5 changed files with 102 additions and 5 deletions
+19 -5
View File
@@ -83,9 +83,23 @@ done
bridge_status=0
wait "$bridge_pid" || bridge_status=$?
bridge_pid=""
for attempt in $(seq 1 8); do
kill -0 "$audio_pid" 2>/dev/null || break
sleep 1
done
# EOF closes the audio stream; keep the replay display alive while it drains.
# Never silently turn a slow/failed audio shutdown into a successful archive.
if [ -n "$audio_pid" ]; then
for attempt in $(seq 1 150); do
kill -0 "$audio_pid" 2>/dev/null || break
sleep .2
done
if kill -0 "$audio_pid" 2>/dev/null; then
echo 'RoadScore audio did not finish within 30 seconds after replay EOF'
exit 1
fi
audio_status=0
wait "$audio_pid" || audio_status=$?
audio_pid=""
if [ "$audio_status" != 0 ]; then
cat results/current/app.log
exit "$audio_status"
fi
fi
exit "$bridge_status"
+1
View File
@@ -61,6 +61,7 @@ env['ROADSCORE_COMPOSITION_POLICY']=composition_policy
env['ROADSCORE_COMPOSER']=a.composer
env['ROADSCORE_ACE_PROFILE']=a.profile
if native:env.setdefault('ROADSCORE_REPLAY_PRIME','1')
if native and not a.replay:env['ROADSCORE_AUDIO_DRAIN_FILE']=str(R/'results/current/audio_drained.json')
env['ROADSCORE_OVERLAY_CAPTURE']=str(out/'overlay.png')
env['ROADSCORE_STATUS_FILE']=str(out/'roadscore_status.json')
initial_display={'readiness':'Preparing','composer':a.composer,'profile':a.profile if a.composer=='ace' else None,'render_mode':a.render_mode,'style':a.profile.title() if a.composer=='ace' else settings.style}
+8
View File
@@ -3,6 +3,14 @@ import os,json,time,runpy
from pathlib import Path
from openpilot.selfdrive.ui.ui_state import UIState, device
from preparing_awake import PreparationWake
from replay_display_hold import ReplayDisplayHold
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):
result=original_state(self,*args,**kwargs)
self.started=display_hold.apply(self.started)
return result
UIState._update_state=replay_state
preparation_wake=PreparationWake(os.environ.get("ROADSCORE_STATUS_FILE"),Path("/TICI").exists())
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer
@@ -0,0 +1,43 @@
"""Keep only the replay presentation visible while its final audio drains."""
import json
import math
from pathlib import Path
import time
class ReplayDisplayHold:
def __init__(self, marker=None, enabled=False, clock=time.monotonic, maximum_hold=20.):
self.marker = Path(marker) if marker else None
self.enabled = bool(enabled and marker)
self.clock = clock
self.started_at = clock()
self.maximum_hold = maximum_hold
self.seen_onroad = False
self.holding_since = None
self.released = False
def apply(self, started):
if not self.enabled or self.released:
return started
now = self.clock()
try:
value = json.loads(self.marker.read_text())
wall = value.get('wall')
if (isinstance(wall, (int, float)) and math.isfinite(wall)
and self.started_at <= wall <= now and type(value.get('drained')) is bool):
self.released = True
return started
except (OSError, ValueError, TypeError):
pass
if started:
self.seen_onroad = True
self.holding_since = None
return True
if not self.seen_onroad:
return False
if self.holding_since is None:
self.holding_since = now
if now - self.holding_since >= self.maximum_hold:
self.released = True
return False
return True
@@ -0,0 +1,31 @@
import json
from pathlib import Path
import tempfile
import unittest
from replay_display_hold import ReplayDisplayHold
class DisplayHoldTests(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup)
self.path=Path(self.tmp.name)/'audio_drained.json';self.now=100.
self.hold=ReplayDisplayHold(self.path,True,lambda:self.now)
def marker(self,wall,drained=True):self.path.write_text(json.dumps(dict(wall=wall,drained=drained)))
def test_actual_onroad_required(self):
self.assertFalse(self.hold.apply(False));self.assertTrue(self.hold.apply(True))
self.now+=1;self.assertTrue(self.hold.apply(False))
self.now+=2;self.marker(self.now);self.assertFalse(self.hold.apply(False))
def test_stale_marker_ignored_and_timeout_bounded(self):
self.marker(99);self.hold.apply(True);self.assertTrue(self.hold.apply(False))
self.now+=20;self.assertFalse(self.hold.apply(False))
def test_failure_releases_and_cannot_relatch(self):
self.hold.apply(True);self.marker(100,False)
self.assertFalse(self.hold.apply(False));self.assertTrue(self.hold.apply(True));self.assertFalse(self.hold.apply(False))
def test_disabled_never_holds(self):
hold=ReplayDisplayHold(self.path,False,lambda:self.now)
self.assertTrue(hold.apply(True));self.assertFalse(hold.apply(False))
def test_partial_and_future_marker_do_not_release(self):
self.hold.apply(True);self.path.write_text('{')
self.assertTrue(self.hold.apply(False));self.marker(200)
self.assertTrue(self.hold.apply(False))
if __name__=='__main__':unittest.main()