From f7d982fbb41f840a2d4b4a15d79da55a834ca24a Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:46:19 -0500 Subject: [PATCH] Connect Galaxy live RoadScore controls to automatic playback --- roadscore/prototype/app.py | 2 +- roadscore/prototype/live_owned_processes.py | 5 ++-- roadscore/prototype/live_target_adapter.py | 2 +- roadscore/prototype/live_target_daemon.py | 11 +++++-- .../prototype/test_live_target_adapter.py | 30 +++++++++++++++++++ .../assets/mobile/js/views/RoadScore.js | 26 +++------------- starpilot/system/the_galaxy/roadscore.py | 16 ++++++++-- .../the_galaxy/tests/test_roadscore_live.py | 11 +++++++ 8 files changed, 72 insertions(+), 31 deletions(-) diff --git a/roadscore/prototype/app.py b/roadscore/prototype/app.py index 461f8b3a75..7272f8c7ae 100644 --- a/roadscore/prototype/app.py +++ b/roadscore/prototype/app.py @@ -33,7 +33,7 @@ run=root/'results/current';run.mkdir(parents=True,exist_ok=True) presentation_session_id=f'{os.getpid()}-{time.monotonic_ns()}' demo_engagement=DemoEngagement(run,presentation_session_id,a.input) output_metadata={'bluetooth_selected':False,'muted':a.mute} -if Path('/TICI').exists() and not a.mute and os.environ.get('ROADSCORE_PCM_RETURN')!='1': +if Path('/TICI').exists() and not a.mute and os.environ.get('ROADSCORE_PCM_RETURN')!='1' and os.environ.get('ROADSCORE_OUTPUT_POLICY')!='system-default': from bluetooth_output import prepare_output output_metadata.update(prepare_output(run)) import sounddevice as sd diff --git a/roadscore/prototype/live_owned_processes.py b/roadscore/prototype/live_owned_processes.py index 0b0ad0768a..40b7fa727b 100644 --- a/roadscore/prototype/live_owned_processes.py +++ b/roadscore/prototype/live_owned_processes.py @@ -33,9 +33,9 @@ class OwnedLiveProcesses: with (generated/'gpu.lock').open('a') as check: fcntl.flock(check,fcntl.LOCK_EX|fcntl.LOCK_NB) config=read(generated/'live_config.json') - profile=config.get('profile','prism') + profile=read(generated/'operator_settings.json').get('profile',config.get('profile','prism')) if profile not in ('prism','aurora'):raise ValueError('Unsupported live profile') - bank=Path(config.get('plan_bank','/data/roadscore-event-assets/conditioning/current')) + bank=Path(config.get('plan_banks',{}).get(profile,config.get('plan_bank','/data/roadscore-event-assets/conditioning/current'))) from cached_composition import validate_bank,digest validate_bank(bank,profile=profile) self.bank_hash=digest(bank/'bank.json') @@ -85,6 +85,7 @@ class OwnedLiveProcesses: env=self.env.copy();env['ROADSCORE_FORCE_MUTE']='0' if audible else '1' env['PYTHONPATH']=':'.join(['/data/openpilot',str(self.root/'prototype'),'/data/roadscore-feasibility/venv/lib/python3.12/site-packages']) env['ROADSCORE_LIVE_SESSION_ID']=session_id + env['ROADSCORE_OUTPUT_POLICY']='system-default' log=(self.folder/'app.log').open('ab');self.logs.append(log) self.app_started=self.clock() self.app=self.popen(['/usr/local/venv/bin/python','-u',str(self.root/'prototype/app.py'),'--root',str(self.root),'--input','live']+(['--audible'] if audible else []),cwd='/data/openpilot',env=env,stdout=log,stderr=log,stdin=subprocess.DEVNULL,start_new_session=True) diff --git a/roadscore/prototype/live_target_adapter.py b/roadscore/prototype/live_target_adapter.py index 9e89278281..1576a294d0 100644 --- a/roadscore/prototype/live_target_adapter.py +++ b/roadscore/prototype/live_target_adapter.py @@ -62,7 +62,7 @@ class TargetAdapter: return self.collector.collect() def enable(self,observation=None,authorization=None): - self._ensure();return self._call('enable') + self._ensure();return self._call('enable',auto_play=True) def prepare_diagnostic(self): self._ensure();return self._call('diagnostic') diff --git a/roadscore/prototype/live_target_daemon.py b/roadscore/prototype/live_target_daemon.py index 6d32d103f6..0c9901b7db 100644 --- a/roadscore/prototype/live_target_daemon.py +++ b/roadscore/prototype/live_target_daemon.py @@ -18,7 +18,7 @@ from live_owned_processes import OwnedLiveProcesses class Engine: def __init__(self,root,collector,owned=None,*,clock=time.monotonic): self.root=Path(root);self.collector=collector;self.owned=owned or OwnedLiveProcesses(root) - self.clock=clock;self.diagnostic=False;self.audible=False;self.stop_event=threading.Event();self.commands=queue.Queue() + self.clock=clock;self.auto_play=False;self.diagnostic=False;self.audible=False;self.stop_event=threading.Event();self.commands=queue.Queue() self.owned.cancel_check=self.stop_event.is_set self.supervisor=LiveSupervisor(self._prepare,self._app,self.owned.stop_owned,clock=clock) self.observation=None;self.authorization=None;self.health_error='No live observations yet';self.started=None;self.last_recorded=0.;self.diagnostic_ready_at=None @@ -43,6 +43,7 @@ class Engine: def status(self): return {**self.supervisor.status(),'available':True,'diagnostic':self.diagnostic,'health_error':self.health_error, + 'parked':bool(self.observation is not None and self.observation.parked and 0<=self.clock()-self.observation.monotonic<=1.), 'can_prepare_diagnostic':not self.supervisor.enabled and self.diagnostic_ok(), 'metrics':self.collector.last_snapshot} @@ -56,6 +57,7 @@ class Engine: if action not in ('enable','diagnostic','driver_ready'):raise ValueError('Unknown live action') if self.stop_event.is_set():raise RuntimeError('Owned live processes are stopping') self.refresh() + if action=='enable' and type(payload.get('auto_play',False)) is not bool:raise ValueError('auto_play must be boolean') if action=='driver_ready': if type(payload.get('audible',False)) is not bool:raise ValueError('audible must be boolean') if self.diagnostic:raise RuntimeError('Diagnostic preparation cannot start playback or authorize driving') @@ -65,12 +67,13 @@ class Engine: if action=='enable' and self.diagnostic and self.supervisor.state=='READY': reason=blocked_reason(self.observation,self.authorization,self.clock(),require_parked=True) if reason:raise RuntimeError(reason) - self.diagnostic=False;self.owned.diagnostic=False;self.supervisor.reason='Verified live preparation; explicit current driver-ready confirmation required' + self.auto_play=payload.get('auto_play',False);self.audible=self.auto_play + self.diagnostic=False;self.owned.diagnostic=False;self.supervisor.reason=('Starting prepared music' if self.auto_play else 'Verified live preparation; explicit current driver-ready confirmation required') if getattr(self.owned,'folder',None) is not None: (self.owned.folder/'production_authorization.json').write_text(json.dumps({'wall':time.time(),'session_id':self.supervisor.session_id,'authorization':asdict(self.authorization)})) return self.status() raise RuntimeError('A live session already exists; stop it before changing mode') - self.diagnostic=action=='diagnostic';self.audible=False;self.diagnostic_ready_at=None + self.diagnostic=action=='diagnostic';self.auto_play=action=='enable' and payload.get('auto_play',False);self.audible=self.auto_play;self.diagnostic_ready_at=None if self.diagnostic: if not self.diagnostic_ok():raise RuntimeError('Fresh healthy parked diagnostic preflight is required') # This authorizes only muted worker preparation, not production capability. @@ -106,6 +109,8 @@ class Engine: if self.supervisor.state=='LIVE' and not health['app_ready']: self.supervisor.stop('Live app status is stale or playback has stopped');return self.supervisor.tick(self.observation,self.authorization,**health) + if self.auto_play and self.supervisor.state=='READY' and not self.stop_event.is_set(): + self.supervisor.confirm_driver_ready(self.supervisor.session_id,self.observation,self.authorization) if self.supervisor.state=='STARTING' and self.clock()-getattr(self.owned,'app_started',self.clock())>30: self.supervisor.stop('Live app failed to establish current input/playback readiness') if self.started is not None and self.clock()-self.started>1500 and self.supervisor.state=='PREPARING': diff --git a/roadscore/prototype/test_live_target_adapter.py b/roadscore/prototype/test_live_target_adapter.py index bb74d767f7..a90445f85d 100644 --- a/roadscore/prototype/test_live_target_adapter.py +++ b/roadscore/prototype/test_live_target_adapter.py @@ -142,3 +142,33 @@ def test_ready_file_alone_does_not_claim_live_playback(tmp_path): assert owned.health()['app_ready'] (current/'status.json').write_text(json.dumps({'route':'live','command_wall':owned.clock()-3,'elapsed':1})) assert not owned.health()['app_ready'] + + +def test_auto_play_starts_audibly_only_after_accepted_buffer(engine): + engine.owned.app_started=10. + engine.owned.health.return_value['accepted_ready']=False + engine.command({'command':'enable','auto_play':True}) + engine.tick() + engine.owned.start_app.assert_not_called() + engine.owned.health.return_value['accepted_ready']=True + engine.tick() + engine.owned.start_app.assert_called_once_with(engine.supervisor.session_id,audible=True) + assert engine.supervisor.state=='STARTING' + engine.owned.health.return_value['app_ready']=True + engine.tick() + assert engine.supervisor.state=='LIVE' + + +def test_auto_play_does_not_bypass_parked_health(engine): + engine.command({'command':'enable','auto_play':True}) + engine.collector.observation=replace(GOOD,parked=False) + engine.tick() + engine.owned.start_app.assert_not_called() + assert not engine.supervisor.enabled + + +def test_diagnostic_ignores_auto_play_request(engine): + engine.command({'command':'diagnostic','auto_play':True}) + engine.tick() + engine.owned.start_app.assert_not_called() + assert engine.supervisor.state=='READY' diff --git a/starpilot/system/the_galaxy/assets/mobile/js/views/RoadScore.js b/starpilot/system/the_galaxy/assets/mobile/js/views/RoadScore.js index 548fda7b1b..4bb7693ed2 100644 --- a/starpilot/system/the_galaxy/assets/mobile/js/views/RoadScore.js +++ b/starpilot/system/the_galaxy/assets/mobile/js/views/RoadScore.js @@ -127,36 +127,18 @@ export const RoadScore = { template: `
Music shaped by the road.
+Music shaped by the road. Plays through the system’s default audio output.
{{status.live?.reason || 'Live driving readiness has not been confirmed.'}}
+{{status.live?.reason || (status.live?.state === 'LIVE' ? 'Music is reacting to live road signals.' : status.live?.can_enable ? 'Ready to enable.' : 'Live readiness has not been confirmed.')}}
Replay simulation changes displayed engagement, turn signals and music. It does not engage or control the vehicle.
-{{status.demo?.available ? (status.demo.mode === 'engaged' ? 'Simulated engage' : status.demo.mode === 'disengaged' ? 'Simulated disengage' : 'Using recorded engagement') : 'Replay unavailable'}}
-{{status.demo?.signal_mode === 'recorded' ? 'Using recorded signals' : 'Simulated signals: ' + (status.demo?.signal_mode || 'recorded')}}
-{{status.demo?.reason}}
-{{ status.locked ? 'Controls unlock when the output service confirms playback and judging are idle.' : 'Use the normal RoadScore launcher to prepare or change styles.' }}
+ +Choose a style before enabling. RoadScore prepares and starts automatically when ready; stop it before changing styles.