mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-04 07:03:44 +08:00
150 lines
4.3 KiB
Python
150 lines
4.3 KiB
Python
import queue
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
|
|
import numpy as np
|
|
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.swaglog import cloudlog
|
|
|
|
|
|
ADDRESS_RE = re.compile(r"^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$")
|
|
|
|
|
|
class BluetoothAudioSink:
|
|
def __init__(self, params: Params | None = None, popen_factory=subprocess.Popen, start_thread: bool = True):
|
|
self.params = params or Params()
|
|
self._popen_factory = popen_factory
|
|
self._queue: queue.Queue[bytes] = queue.Queue(maxsize=3)
|
|
self._lock = threading.Lock()
|
|
self._process = None
|
|
self._address = ""
|
|
self._healthy = False
|
|
self._last_write = 0.0
|
|
self._exit = False
|
|
self._aplay = shutil.which("aplay")
|
|
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
if start_thread:
|
|
self._thread.start()
|
|
|
|
@property
|
|
def healthy(self) -> bool:
|
|
if not self._lock.acquire(blocking=False):
|
|
return False
|
|
try:
|
|
process_alive = self._process is not None and self._process.poll() is None
|
|
return self._healthy and process_alive and time.monotonic() - self._last_write < 1.0
|
|
finally:
|
|
self._lock.release()
|
|
|
|
def close(self) -> None:
|
|
self._exit = True
|
|
self._stop_process()
|
|
if self._thread.is_alive():
|
|
self._thread.join(timeout=1.0)
|
|
|
|
def desired_address(self) -> str:
|
|
if not self.params.get_bool("BluetoothEnabled"):
|
|
return ""
|
|
address = (self.params.get("BluetoothAudioAddress", encoding="utf-8") or "").strip().upper()
|
|
if isinstance(address, bytes):
|
|
address = address.decode("utf-8", errors="ignore")
|
|
return address if ADDRESS_RE.fullmatch(address) else ""
|
|
|
|
@staticmethod
|
|
def pcm_bytes(samples: np.ndarray) -> bytes:
|
|
mono = np.clip(samples, -1.0, 1.0)
|
|
pcm = (mono * 32767.0).astype(np.int16)
|
|
return np.column_stack((pcm, pcm)).tobytes()
|
|
|
|
def submit(self, samples: np.ndarray) -> bool:
|
|
if self._aplay is None or not self._address:
|
|
return False
|
|
try:
|
|
self._queue.put_nowait(self.pcm_bytes(samples))
|
|
except queue.Full:
|
|
with self._lock:
|
|
self._healthy = False
|
|
return False
|
|
return self.healthy
|
|
|
|
def _start_process(self, address: str) -> None:
|
|
command = [
|
|
self._aplay,
|
|
"-q",
|
|
"-D", f"bluealsa:DEV={address},PROFILE=a2dp",
|
|
"-t", "raw",
|
|
"-f", "S16_LE",
|
|
"-c", "2",
|
|
"-r", "48000",
|
|
]
|
|
process = self._popen_factory(command, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, bufsize=0)
|
|
with self._lock:
|
|
self._process = process
|
|
self._address = address
|
|
self._healthy = False
|
|
self._last_write = 0.0
|
|
|
|
def _stop_process(self) -> None:
|
|
with self._lock:
|
|
process = self._process
|
|
self._process = None
|
|
self._address = ""
|
|
self._healthy = False
|
|
self._last_write = 0.0
|
|
if process is not None:
|
|
try:
|
|
process.terminate()
|
|
process.wait(timeout=1.0)
|
|
except Exception:
|
|
try:
|
|
process.kill()
|
|
except Exception:
|
|
pass
|
|
while True:
|
|
try:
|
|
self._queue.get_nowait()
|
|
except queue.Empty:
|
|
break
|
|
|
|
def _run(self) -> None:
|
|
while not self._exit:
|
|
address = self.desired_address()
|
|
with self._lock:
|
|
current_address = self._address
|
|
process = self._process
|
|
if not address or self._aplay is None:
|
|
if process is not None:
|
|
self._stop_process()
|
|
time.sleep(0.2)
|
|
continue
|
|
if process is None or process.poll() is not None or address != current_address:
|
|
self._stop_process()
|
|
try:
|
|
self._start_process(address)
|
|
except Exception:
|
|
cloudlog.exception("Unable to start Bluetooth audio output")
|
|
time.sleep(1.0)
|
|
continue
|
|
|
|
try:
|
|
block = self._queue.get(timeout=0.5)
|
|
except queue.Empty:
|
|
continue
|
|
try:
|
|
with self._lock:
|
|
process = self._process
|
|
if process is None or process.stdin is None:
|
|
raise BrokenPipeError
|
|
process.stdin.write(block)
|
|
with self._lock:
|
|
self._healthy = True
|
|
self._last_write = time.monotonic()
|
|
except Exception:
|
|
cloudlog.warning("Bluetooth audio output disconnected")
|
|
self._stop_process()
|
|
time.sleep(0.5)
|