mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-04 13:24:13 +08:00
Capture saved replay screens only for explicit mirror mode
This commit is contained in:
@@ -52,6 +52,7 @@ class DemoSession:
|
||||
pid = owner.get('pid')
|
||||
running = type(pid) is int and pid > 1 and self.ticks(pid) == owner.get('start_ticks') and owner.get('start_ticks') is not None
|
||||
result = {key: owner.get(key) for key in ('request_id', 'alias', 'route', 'pid', 'out', 'muted')}
|
||||
result['screen_mirror'] = owner.get('screen_mirror') is True
|
||||
result.update(running=bool(running), generation_invoked=False)
|
||||
if owner.get('out'):
|
||||
out = Path(owner['out'])
|
||||
@@ -65,17 +66,20 @@ class DemoSession:
|
||||
return result
|
||||
|
||||
def start(self, data):
|
||||
if (not isinstance(data, dict) or set(data) != {'alias', 'request_id', 'muted'}
|
||||
required = {'alias', 'request_id', 'muted'}
|
||||
if (not isinstance(data, dict) or not required <= set(data) or set(data) - required - {'screen_mirror'}
|
||||
or not isinstance(data['request_id'], str) or not re.fullmatch(r'[a-zA-Z0-9-]{8,80}', data['request_id'])
|
||||
or type(data['muted']) is not bool):
|
||||
raise ValueError('Saved demo requires an alias, request ID and explicit output choice')
|
||||
or type(data['muted']) is not bool or type(data.get('screen_mirror', False)) is not bool):
|
||||
raise ValueError('Saved demo requires an alias, request ID, explicit output choice and optional screen_mirror boolean')
|
||||
data = {**data, 'screen_mirror': data.get('screen_mirror', False)}
|
||||
if not self.offroad():
|
||||
raise ValueError('Saved demo playback requires the vehicle to be offroad')
|
||||
selected = entry(self.root, data['alias'])
|
||||
with self.lock:
|
||||
current = self.status()
|
||||
if current['running']:
|
||||
if current['request_id'] == data['request_id'] and current['alias'] == data['alias'] and current['muted'] == data['muted']:
|
||||
if (current['request_id'] == data['request_id'] and current['alias'] == data['alias']
|
||||
and current['muted'] == data['muted'] and current['screen_mirror'] == data['screen_mirror']):
|
||||
return current
|
||||
raise ValueError('A saved demo is already running; stop that owned session first')
|
||||
if read(self.owner_path).get('request_id') == data['request_id']:
|
||||
@@ -91,8 +95,9 @@ class DemoSession:
|
||||
'--score-archive', selected['archive']]
|
||||
if selected.get('curve_plan'): command += ['--curve-plan', selected['curve_plan']]
|
||||
if data['muted']: command.append('--muted')
|
||||
if data['screen_mirror']: command.append('--screen-mirror')
|
||||
env = os.environ.copy()
|
||||
for key in ('ZMQ', 'OPENPILOT_PREFIX', 'OPENPILOT_ZMQ_NAMESPACE', 'PARAMS_ROOT'):
|
||||
for key in ('ZMQ', 'OPENPILOT_PREFIX', 'OPENPILOT_ZMQ_NAMESPACE', 'PARAMS_ROOT', 'ROADSCORE_MIRROR_DIR'):
|
||||
env.pop(key, None)
|
||||
env['PYTHONPATH'] = ':'.join((str(self.root/'prototype'), '/data/openpilot', '/data/roadscore-feasibility/venv/lib/python3.12/site-packages'))
|
||||
with (out/'launcher.log').open('wb') as log:
|
||||
|
||||
@@ -67,14 +67,14 @@ def start_deadline(path, session, now):
|
||||
except (OSError, ValueError):return None
|
||||
|
||||
|
||||
def native_environment(project, root, out, session, inherited=None):
|
||||
def native_environment(project, root, out, session, inherited=None, *, screen_mirror=False):
|
||||
env = dict(os.environ if inherited is None else inherited)
|
||||
for key in ('ZMQ','PARAMS_ROOT','OPENPILOT_ZMQ_NAMESPACE','ROADSCORE_PCM_RETURN','ROADSCORE_RESIDENT','ROADSCORE_GENERATION_SEED','ROADSCORE_SEED_ORIGIN'):
|
||||
for key in ('ZMQ','PARAMS_ROOT','OPENPILOT_ZMQ_NAMESPACE','ROADSCORE_PCM_RETURN','ROADSCORE_RESIDENT','ROADSCORE_GENERATION_SEED','ROADSCORE_SEED_ORIGIN','ROADSCORE_MIRROR_DIR'):
|
||||
env.pop(key, None)
|
||||
env.update(PYTHONDONTWRITEBYTECODE='1',OPENPILOT_PREFIX='roadscore_replay',BASEDIR=str(project),PWD=str(project),NOBOARD='1',SIMULATION='1',SKIP_FW_QUERY='1',BIG='0',ROADSCORE_PREPARED_SHOWCASE='1',ROADSCORE_SHOWCASE_SESSION=session,ROADSCORE_REPLAY_UI_CONTROLS='1',ROADSCORE_CLEAN_DEMO_UI='1',ROADSCORE_OVERLAY='1',ROADSCORE_STATUS_FILE=str(out/'status.json'),ROADSCORE_UI_AUDIT=str(out/'ui_audit.jsonl'),ROADSCORE_OVERLAY_CAPTURE=str(out/'overlay.png'),ROADSCORE_AUDIO_DRAIN_FILE=str(out/'audio_drained.json'),ROADSCORE_PRESENTATION_POLICY='conservative-v4',SP_ALLOW_DESKTOP_FAKE_WIFI='0',SP_ALLOW_DESKTOP_FAKE_BLUETOOTH='0',SP_ONROAD_NAV_DEMO='0',SP_ONROAD_CEM_DEMO='0')
|
||||
paths=[HERE,project,project/'starpilot/third_party',*project.glob('*_repo'),Path('/data/roadscore-feasibility/venv/lib/python3.12/site-packages')]
|
||||
env['PYTHONPATH']=':'.join(map(str,paths))
|
||||
env['ROADSCORE_MIRROR_DIR']=str(out/'mirror')
|
||||
if screen_mirror:env['ROADSCORE_MIRROR_DIR']=str(out/'mirror')
|
||||
env['ROADSCORE_REPLAY_PRIME']='1'
|
||||
return env
|
||||
|
||||
@@ -132,6 +132,7 @@ def main():
|
||||
p.add_argument('--project-root',type=Path,default=Path('/data/openpilot'))
|
||||
p.add_argument('--score-archive',type=Path);p.add_argument('--curve-plan',type=Path)
|
||||
p.add_argument('--out',type=Path);p.add_argument('--muted',action='store_true');p.add_argument('--headless',action='store_true')
|
||||
p.add_argument('--screen-mirror',action='store_true',help='Capture the native UI for the legacy screen-mirror viewer')
|
||||
p.add_argument('--check',action='store_true');p.add_argument('--hold-start',action='store_true')
|
||||
p.add_argument('--start-timeout',type=float,default=120.);p.add_argument('--duration',type=float,default=float('inf'))
|
||||
a=p.parse_args()
|
||||
@@ -163,7 +164,7 @@ def main():
|
||||
for path in (py,replay,project/'selfdrive/ui/ui.py',HERE/'mac_showcase.py'):
|
||||
if not path.is_file():raise FileNotFoundError(path)
|
||||
args=replay_arguments(meta,local)
|
||||
checked=dict(route=route,archive=str(archive),seconds=duration,local_cache=str(local),generation_invoked=False,requires_chestnut=False,replay_args=args)
|
||||
checked=dict(route=route,archive=str(archive),seconds=duration,local_cache=str(local),generation_invoked=False,requires_chestnut=False,replay_args=args,screen_mirror=a.screen_mirror)
|
||||
if a.check:
|
||||
print(json.dumps(checked,indent=2));return
|
||||
from native_ownership import verify_offroad,DisplayOwner
|
||||
@@ -187,7 +188,7 @@ def main():
|
||||
if any(path.name!='launcher.log' for path in out.iterdir()):raise ValueError('Prepared output directory must contain only its supervisor launcher.log')
|
||||
else:out.mkdir(parents=True)
|
||||
previous=install_current(root,out,session)
|
||||
env=native_environment(project,root,out,session)
|
||||
env=native_environment(project,root,out,session,screen_mirror=a.screen_mirror)
|
||||
env['ROADSCORE_FORCE_MUTE']='1' if muted else '0'
|
||||
Path('/dev/shm/msgq_roadscore_replay').mkdir(exist_ok=True)
|
||||
launch={**checked,'mode':'prepared-interactive-showcase','session_id':session,'muted':muted,'core_sha256':hashlib.sha256((archive/'dry.wav').read_bytes()).hexdigest(),'previous_current':str(previous) if previous else None,'curve_plan':str(a.curve_plan) if a.curve_plan else None}
|
||||
|
||||
@@ -114,7 +114,7 @@ def mirror_main():
|
||||
signal.signal(signal.SIGTERM,interrupt)
|
||||
try:
|
||||
launched=True
|
||||
peer.call('demo_start',{'alias':args.route,'request_id':request_id,'muted':args.muted})
|
||||
peer.call('demo_start',{'alias':args.route,'request_id':request_id,'muted':args.muted,'screen_mirror':True})
|
||||
url=f'http://127.0.0.1:{server.server_port}'
|
||||
print('RoadScore saved demo. No model generation. Controls and mirrored screen: '+url,flush=True)
|
||||
if not args.no_browser:subprocess.Popen(['/usr/bin/open',url],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
@@ -231,8 +231,10 @@ def native_pair_main():
|
||||
try:
|
||||
launched=True
|
||||
peer.call('demo_start',{'alias':args.route,'request_id':request_id,'muted':args.muted})
|
||||
env=os.environ.copy()
|
||||
env.pop('ROADSCORE_MIRROR_DIR',None)
|
||||
with (out/'mac.log').open('wb') as log:
|
||||
process=subprocess.Popen(command,cwd=project,stdin=subprocess.DEVNULL,stdout=log,
|
||||
process=subprocess.Popen(command,cwd=project,env=env,stdin=subprocess.DEVNULL,stdout=log,
|
||||
stderr=subprocess.STDOUT,start_new_session=True,close_fds=True)
|
||||
print('Preparing saved RoadScore on comma and native Mac UI. No model generation.',flush=True)
|
||||
deadline=time.monotonic()+100
|
||||
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from demo_catalog import entry
|
||||
from demo_session import DemoSession, write
|
||||
@@ -45,12 +46,40 @@ class SavedDemoTests(unittest.TestCase):
|
||||
command,options=self.calls[0]
|
||||
self.assertTrue(command[1].endswith('/native_prepared_showcase.py'))
|
||||
self.assertIn('--hold-start',command);self.assertIn('--muted',command)
|
||||
self.assertNotIn('--screen-mirror',command)
|
||||
self.assertFalse(current['screen_mirror'])
|
||||
self.assertTrue(options['close_fds']);self.assertTrue(options['start_new_session'])
|
||||
with self.assertRaises(ValueError):self.service.start({**self.request,'request_id':'different-request'})
|
||||
with self.assertRaises(ValueError):self.service.stop({'request_id':'different-request'})
|
||||
self.service.stop({'request_id':self.request['request_id']})
|
||||
self.assertEqual(self.stops,[123])
|
||||
|
||||
def test_capture_is_explicit_and_part_of_launch_identity(self):
|
||||
with patch.dict('os.environ',{'ROADSCORE_MIRROR_DIR':'/inherited/mirror'}):
|
||||
current=self.service.start({**self.request,'screen_mirror':True})
|
||||
self.assertTrue(current['screen_mirror'])
|
||||
command,options=self.calls[0]
|
||||
self.assertIn('--screen-mirror',command)
|
||||
self.assertNotIn('ROADSCORE_MIRROR_DIR',options['env'])
|
||||
self.assertEqual(self.service.start({**self.request,'screen_mirror':True})['pid'],123)
|
||||
with self.assertRaises(ValueError):self.service.start(self.request)
|
||||
self.assertEqual(len(self.calls),1)
|
||||
|
||||
def test_optional_capture_defaults_to_false_without_inherited_activation(self):
|
||||
with patch.dict('os.environ',{'ROADSCORE_MIRROR_DIR':'/inherited/mirror'}):
|
||||
current=self.service.start(self.request)
|
||||
self.assertFalse(current['screen_mirror'])
|
||||
self.assertEqual(self.service.start({**self.request,'screen_mirror':False})['pid'],123)
|
||||
self.assertNotIn('ROADSCORE_MIRROR_DIR',self.calls[0][1]['env'])
|
||||
with self.assertRaises(ValueError):self.service.start({**self.request,'screen_mirror':True})
|
||||
|
||||
def test_capture_api_rejects_nonbooleans_and_unknown_fields_before_spawning(self):
|
||||
for value in (None,0,1,'true','false',[],{}):
|
||||
with self.subTest(value=value),self.assertRaises(ValueError):
|
||||
self.service.start({**self.request,'screen_mirror':value})
|
||||
with self.assertRaises(ValueError):self.service.start({**self.request,'mirror':True})
|
||||
self.assertEqual(self.calls,[])
|
||||
|
||||
def test_matching_ready_session_is_required_for_release(self):
|
||||
current=self.service.start(self.request);out=Path(current['out'])
|
||||
request={'request_id':self.request['request_id'],'session_id':'prepared-session'}
|
||||
|
||||
@@ -63,6 +63,19 @@ class NativePreparedTests(unittest.TestCase):
|
||||
self.assertIn('--presentation-root',args);self.assertIn('bluealsa:AA:BB',args)
|
||||
self.assertTrue(args[1].endswith('/mac_showcase.py'))
|
||||
self.assertFalse(any('ace_worker' in item or 'power_worker' in item for item in args))
|
||||
def test_continuous_capture_is_absent_even_with_inherited_mirror_directory(self):
|
||||
from screen_mirror import ScreenMirror
|
||||
inherited={'ROADSCORE_MIRROR_DIR':'/old/session/mirror'}
|
||||
for choice in ({}, {'screen_mirror':False}):
|
||||
env=native_environment(Path(self.tmp.name),self.root,self.out,'session-123',inherited,**choice)
|
||||
self.assertNotIn('ROADSCORE_MIRROR_DIR',env)
|
||||
self.assertIsNone(ScreenMirror.from_environ(env))
|
||||
self.assertEqual(inherited['ROADSCORE_MIRROR_DIR'],'/old/session/mirror')
|
||||
def test_explicit_legacy_mirror_uses_only_this_owned_output(self):
|
||||
env=native_environment(Path(self.tmp.name),self.root,self.out,'session-123',
|
||||
{'ROADSCORE_MIRROR_DIR':'/old/session/mirror'},screen_mirror=True)
|
||||
self.assertEqual(env['ROADSCORE_MIRROR_DIR'],str(self.out/'mirror'))
|
||||
self.assertEqual(env['ROADSCORE_REPLAY_PRIME'],'1')
|
||||
def test_native_replay_uses_only_given_local_cache(self):
|
||||
meta={'native_replay_args':['fixture','--data_dir','old','--start','149']}
|
||||
args=replay_arguments(meta,Path('/local/playback'))
|
||||
|
||||
@@ -3,10 +3,11 @@ from pathlib import Path
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
from urllib.error import HTTPError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from paired_showcase import DemoPeer, control_server, local_start_deadline, mac_command, read_ready
|
||||
from paired_showcase import DemoPeer, control_server, local_start_deadline, mac_command, read_ready, mirror_main, native_pair_main
|
||||
|
||||
|
||||
class NativePairTests(unittest.TestCase):
|
||||
@@ -48,6 +49,34 @@ class NativePairTests(unittest.TestCase):
|
||||
'http://192.168.1.2:8082',Path('/owned/mac'),12.,fullscreen=True)
|
||||
self.assertIn('--fullscreen',command)
|
||||
|
||||
def test_normal_pair_never_requests_or_inherits_screen_capture(self):
|
||||
calls=[]
|
||||
def call(action,data=None):
|
||||
calls.append((action,data))
|
||||
if action=='status':return {'offroad':True}
|
||||
if action=='demo_ready':return dict(request_id='paired-test-request',route='private/route',running=True,prepared=True,ready_session_id='native-session')
|
||||
if action=='demo_play':return dict(request_id='paired-test-request',session_id='native-session',start_at_wall=101.,server_wall=100.)
|
||||
return {}
|
||||
peer=SimpleNamespace(call=call,peer=SimpleNamespace(base_url='http://192.168.8.156:8082'))
|
||||
process=Mock();process.poll.return_value=None
|
||||
def local_ready(path,route):
|
||||
path.parent.mkdir(exist_ok=True)
|
||||
return {'session_id':'mac-session'}
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
root=Path(folder)/'roadscore';(root/'assets').mkdir(parents=True);(root/'prototype').mkdir()
|
||||
(root/'assets/demo_catalog.json').write_text(json.dumps({'paired_comma':peer.peer.base_url}))
|
||||
with patch('paired_showcase.HERE',root/'prototype'),patch('paired_showcase.DemoPeer',return_value=peer), \
|
||||
patch('paired_showcase.signal.signal'),patch('paired_showcase.subprocess.Popen',return_value=process) as spawn, \
|
||||
patch('paired_showcase.stop_mac'),patch('paired_showcase.read_ready',side_effect=local_ready), \
|
||||
patch('paired_showcase.uuid.uuid4',return_value=SimpleNamespace(hex='paired-test-request')), \
|
||||
patch('demo_catalog.entry',return_value={'route':'private/route','archive':'/protected/core'}), \
|
||||
patch('mac_replay_ownership.preflight'),patch.dict('os.environ',{'ROADSCORE_MIRROR_DIR':'/inherited/mirror'}), \
|
||||
patch('sys.argv',['paired_showcase','--muted','--duration','0.000000001']):
|
||||
native_pair_main()
|
||||
self.assertNotIn('ROADSCORE_MIRROR_DIR',spawn.call_args.kwargs['env'])
|
||||
payload=next(data for action,data in calls if action=='demo_start')
|
||||
self.assertNotIn('screen_mirror',payload)
|
||||
|
||||
|
||||
class MirrorControlTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -84,4 +113,29 @@ class MirrorControlTests(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):DemoPeer('http://unrelated.example:8082')
|
||||
|
||||
|
||||
class LegacyMirrorLaunchTests(unittest.TestCase):
|
||||
def test_legacy_viewer_explicitly_requests_native_capture(self):
|
||||
calls=[]
|
||||
def call(action,data=None):
|
||||
calls.append((action,data))
|
||||
if action=='status':return {'offroad':True}
|
||||
if action=='demo_ready':return {'request_id':'mirror-test-request','running':True,'prepared':True,'ready_session_id':'native-session'}
|
||||
return {}
|
||||
peer=SimpleNamespace(call=call)
|
||||
server=Mock(server_port=56976)
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
root=Path(folder);(root/'assets').mkdir();(root/'prototype').mkdir()
|
||||
(root/'assets/demo_catalog.json').write_text(json.dumps({'paired_comma':'http://192.168.8.156:8082'}))
|
||||
with patch('paired_showcase.HERE',root/'prototype'),patch('paired_showcase.DemoPeer',return_value=peer), \
|
||||
patch('paired_showcase.control_server',return_value=server),patch('paired_showcase.signal.signal'), \
|
||||
patch('paired_showcase.uuid.uuid4',return_value=SimpleNamespace(hex='mirror-test-request')), \
|
||||
patch('demo_catalog.entry',return_value={'route':'private/route'}), \
|
||||
patch('sys.argv',['paired_showcase','--screen-mirror','--no-browser','--muted','--duration','0']):
|
||||
mirror_main()
|
||||
payload=next(data for action,data in calls if action=='demo_start')
|
||||
self.assertEqual(payload,{'alias':'route1','request_id':'mirror-test-request','muted':True,'screen_mirror':True})
|
||||
self.assertIn(('demo_stop',{'request_id':'mirror-test-request'}),calls)
|
||||
server.shutdown.assert_called_once();server.server_close.assert_called_once()
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user