From 249eb88b3a397db847e4a595d7e23b7e85d329e5 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:15:14 -0700 Subject: [PATCH] Bind dedicated Bluetooth PCM directly instead of late-overridden ALSA defaults --- roadscore/prototype/bluetooth_output.py | 11 +++++++---- roadscore/prototype/test_bluetooth_output.py | 8 +++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/roadscore/prototype/bluetooth_output.py b/roadscore/prototype/bluetooth_output.py index da22c0f34a..5b959e97f8 100644 --- a/roadscore/prototype/bluetooth_output.py +++ b/roadscore/prototype/bluetooth_output.py @@ -6,6 +6,8 @@ import re import subprocess import sys +PCM_NAME = 'roadscore_bluetooth' + ADDRESS = re.compile(r'^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$') @@ -43,14 +45,15 @@ def prepare_output(directory, *, env=None, reader=real_selection, raise ValueError('A valid system ALSA configuration file is required') path=Path(directory).resolve()/'roadscore-bluealsa.conf' path.parent.mkdir(parents=True, exist_ok=True) - config=f'<{base}>\n\ndefaults.bluealsa.!device "{address}"\ndefaults.bluealsa.!profile "a2dp"\n' + config=(f'<{base}>\n\npcm.{PCM_NAME} {{\n type bluealsa\n device "{address}"\n' + ' profile "a2dp"\n hint { show on description "RoadScore selected Bluetooth speaker" }\n}\n') # Exclusive creation protects existing configuration and keeps the change process-local. with path.open('x') as handle: os.chmod(path, 0o600) handle.write(config) env['ALSA_CONFIG_PATH']=str(path) return {'bluetooth_selected':True, 'address':address, 'output_identity':f'bluealsa:{address}', - 'profile':'a2dp', 'sample_rate':48000, 'channels':2, 'alsa_config':str(path), + 'profile':'a2dp', 'pcm_name':PCM_NAME, 'sample_rate':48000, 'channels':2, 'alsa_config':str(path), 'timing_source':'PortAudio host/backend estimate; not measured acoustic Bluetooth latency', 'physical_latency_ms':None} @@ -59,8 +62,8 @@ def select_device(devices, metadata): if not metadata.get('bluetooth_selected'): return None matches=[index for index, device in enumerate(devices) - if str(device.get('name', '')).strip().lower()=='bluealsa' + if str(device.get('name', '')).strip().lower()==metadata.get('pcm_name', PCM_NAME) and device.get('max_output_channels', 0)>=2] if len(matches)!=1: - raise RuntimeError('Selected Bluetooth speaker requires one stereo BlueALSA output; no speaker fallback') + raise RuntimeError('Selected Bluetooth speaker requires one selected RoadScore Bluetooth PCM; no speaker fallback') return matches[0] diff --git a/roadscore/prototype/test_bluetooth_output.py b/roadscore/prototype/test_bluetooth_output.py index aad5a12cb6..6a756df746 100644 --- a/roadscore/prototype/test_bluetooth_output.py +++ b/roadscore/prototype/test_bluetooth_output.py @@ -13,13 +13,15 @@ class BluetoothOutputTests(unittest.TestCase): meta=prepare_output(Path(d)/'run',env=env,reader=lambda _:dict(enabled=True,address='aa:bb:cc:dd:ee:ff'),alsa_base=base,loaded_modules={}) text=Path(env['ALSA_CONFIG_PATH']).read_text() self.assertIn(f'<{base}>',text) - self.assertIn('defaults.bluealsa.!device "AA:BB:CC:DD:EE:FF"',text) + self.assertIn('pcm.roadscore_bluetooth {',text) + self.assertIn('device "AA:BB:CC:DD:EE:FF"',text) + self.assertNotIn('defaults.bluealsa',text) self.assertEqual(base.read_text(),'system config') self.assertEqual(env['OPENPILOT_PREFIX'],'replay') self.assertIsNone(meta['physical_latency_ms']) - self.assertEqual(select_device([{'name':'default','max_output_channels':2},{'name':'bluealsa','max_output_channels':128}],meta),1) + self.assertEqual(select_device([{'name':'default','max_output_channels':2},{'name':'roadscore_bluetooth','max_output_channels':128}],meta),1) def test_missing_or_ambiguous_bluetooth_does_not_fallback(self): - for devices in [[],[{'name':'default','max_output_channels':2}], [{'name':'bluealsa','max_output_channels':1}], [{'name':'bluealsa','max_output_channels':2}]*2]: + for devices in [[],[{'name':'default','max_output_channels':2}], [{'name':'bluealsa','max_output_channels':128}], [{'name':'roadscore_bluetooth','max_output_channels':1}], [{'name':'roadscore_bluetooth','max_output_channels':2}]*2]: with self.assertRaises(RuntimeError):select_device(devices,{'bluetooth_selected':True}) def test_disabled_does_not_touch_configuration(self): env={};self.assertEqual(prepare_output('/unused',env=env,reader=lambda _:dict(enabled=False)),{'bluetooth_selected':False});self.assertEqual(env,{})