diff --git a/src/BTDevicesManager/BTAudioConnect b/src/BTDevicesManager/BTAudioConnect index 3345abf..c2bae35 100644 --- a/src/BTDevicesManager/BTAudioConnect +++ b/src/BTDevicesManager/BTAudioConnect @@ -124,7 +124,7 @@ log "Reset /etc/asound.conf to default configuration" } bluetooth_asoundconf() { -cat << EOF >> /etc/asound.conf +cat << EOF >> "$ASOUND_TARGET" pcm.bluetooth { type plug slave.pcm { @@ -143,15 +143,29 @@ ctl.bluetooth { type bluealsa } EOF -log "Added Bluetooth device '$2' ($1) to /etc/asound.conf" +log "Added Bluetooth device '$2' ($1) to $ASOUND_TARGET" } +# Dreambox One/Two decode audio in software: enigma2 plays to the +# Bluetooth sink itself, so keep the box asound.conf and skip the +# capture/playback pipe. +SOFT_BTAUDIO=0 +case "$(grep -m 1 '^model=' /usr/lib/enigma.info 2>/dev/null | cut -d'=' -f2 | tr -d "'\"")" in + dreamone|dreamtwo) SOFT_BTAUDIO=1 ;; +esac + rm -f "$STREAM_TOKENFILE" stop_pcm "$PLAYBACK_PIDFILE" stop_pcm "$CAPTURE_PIDFILE" -log "Applying default sound configuration" -default_asoundconf +if [ "$SOFT_BTAUDIO" = "1" ]; then + ASOUND_TARGET=/etc/alsa/conf.d/90-btdevicesmanager.conf + rm -f "$ASOUND_TARGET" +else + ASOUND_TARGET=/etc/asound.conf + log "Applying default sound configuration" + default_asoundconf +fi if [ -n "$1" ]; then start_dbus || exit 1 @@ -183,8 +197,14 @@ if [ -n "$1" ]; then bluetoothname="$(bluetoothctl info "$bluetoothaddr" | grep "Name" | cut -d ':' -f 2 | xargs)" log "Device name detected: $bluetoothname" + [ "$SOFT_BTAUDIO" = "1" ] && mkdir -p "$(dirname "$ASOUND_TARGET")" bluetooth_asoundconf "$bluetoothaddr" "$bluetoothname" + if [ "$SOFT_BTAUDIO" = "1" ]; then + log "Script finished" + exit 0 + fi + log "Starting audio streaming to Bluetooth device" printf '%s\n' "$$" > "$STREAM_TOKENFILE" capture_audio "$$" 9>&- | aplay -q -f dat --buffer-size=16384 --period-size=2048 -t raw -D bluetooth --process-id-file "$PLAYBACK_PIDFILE" 9>&- & diff --git a/src/BTDevicesManager/bluetoothctl.py b/src/BTDevicesManager/bluetoothctl.py index f0e0f02..ce3c1e4 100644 --- a/src/BTDevicesManager/bluetoothctl.py +++ b/src/BTDevicesManager/bluetoothctl.py @@ -21,6 +21,7 @@ # You should have received a copy of the GNU General Public License # along with ReachView. If not, see . +from os import listdir from pexpect import EOF, TIMEOUT, spawnu from re import compile from subprocess import check_output @@ -46,13 +47,31 @@ def __init__(self): self.next_retry_at = 0 self.start_thread = None self.start_lock = threading.Lock() - try: - check_output("rfkill unblock bluetooth", shell=True) - except Exception as e: - self.start_error = f"rfkill unblock bluetooth failed: {e}" - print(f"[BluetoothManager] {self.start_error}") + # Some drivers (Amlogic bt-dev) power-cycle the controller on every + # unblock, which drops the firmware loaded by hciattach. + if self._bluetooth_blocked(): + try: + check_output("rfkill unblock bluetooth", shell=True) + except Exception as e: + self.start_error = f"rfkill unblock bluetooth failed: {e}" + print(f"[BluetoothManager] {self.start_error}") self._start_thread() + @staticmethod + def _bluetooth_blocked(): + try: + for name in listdir("/sys/class/rfkill"): + path = f"/sys/class/rfkill/{name}" + with open(f"{path}/type") as fd: + if fd.read().strip() != "bluetooth": + continue + with open(f"{path}/soft") as fd: + if fd.read().strip() == "1": + return True + except OSError: + return True + return False + def _start_thread(self, force=False): now = monotonic() if not force and self.next_retry_at and now < self.next_retry_at: diff --git a/src/BTDevicesManager/plugin.py b/src/BTDevicesManager/plugin.py index 80d378d..845b859 100644 --- a/src/BTDevicesManager/plugin.py +++ b/src/BTDevicesManager/plugin.py @@ -23,12 +23,15 @@ from signal import SIGUSR2 from twisted.internet.reactor import callInThread -from enigma import eTimer, iPlayableService +from enigma import eServiceReference, eTimer, iPlayableService + +import NavigationInstance from Components.Label import Label from Components.Sources.StaticText import StaticText from Components.ActionMap import ActionMap from Components.config import config, ConfigText, ConfigSubsection, ConfigYesNo +from Components.Console import Console from Components.MenuList import MenuList from Components.ServiceEventTracker import ServiceEventTracker from Plugins.Plugin import PluginDescriptor @@ -44,9 +47,73 @@ config.btdevicesmanager.audioconnect = ConfigYesNo(default=False) config.btdevicesmanager.audioaddress = ConfigText(default="", fixed_size=False) +BTAUDIO_PROC = "/proc/stb/audio/btaudio" + + +def isSoftBTAudio(): + # Dreambox One/Two decode audio in software; enigma2 plays to the + # Bluetooth ALSA sink when the audio source is set to Bluetooth. + try: + with open("/usr/lib/enigma.info") as fd: + for line in fd: + if line.startswith("model="): + return line.split("=", 1)[1].strip().strip("'\"") in ("dreamone", "dreamtwo") + except OSError: + pass + return False + + +SOFT_BTAUDIO = isSoftBTAudio() +softBTAudioConsole = Console() + + +def setSoftBTAudioOutput(enabled): + audioSource = getattr(config.av, "audio_source", None) + if audioSource is None: + return + previous = audioSource.value + if enabled: + audioSource.value = "2" + elif audioSource.value == "2": + audioSource.value = "0" + audioSource.save() + if audioSource.value != previous: + restartMediaService() + + +def restartMediaService(): + # DVB follows the audio source on the fly, the GStreamer sink only picks + # its ALSA device when the service starts. + nav = NavigationInstance.instance + ref = nav and nav.getCurrentlyPlayingServiceReference() + if ref and ref.type != eServiceReference.idDVB: + nav.restartService() + + +def softBTAudioConnected(result, retval, extraArgs=None): + if retval == 0: + setSoftBTAudioOutput(True) + else: + print(f"[BluetoothManager] Bluetooth audio connect failed: {retval}") + + +def applySoftBTAudioState(): + config.btdevicesmanager.audioaddress.save() + config.btdevicesmanager.audioconnect.save() + commandconnect = resolveFilename(SCOPE_CURRENT_PLUGIN, "Extensions/BTDevicesManager/BTAudioConnect") + audioaddress = config.btdevicesmanager.audioaddress.value if config.btdevicesmanager.audioconnect.value else "" + if audioaddress: + softBTAudioConsole.ePopen(f"{commandconnect} {audioaddress}", softBTAudioConnected) + else: + setSoftBTAudioOutput(False) + softBTAudioConsole.ePopen(commandconnect) + def applyBTAudioState(): - if not isfile("/proc/stb/audio/btaudio"): + if SOFT_BTAUDIO: + applySoftBTAudioState() + return + if not isfile(BTAUDIO_PROC): return newState = "off" @@ -60,7 +127,7 @@ def applyBTAudioState(): config.av.btaudio.save() try: - with open("/proc/stb/audio/btaudio", "w") as fn: + with open(BTAUDIO_PROC, "w") as fn: fn.write(newState) except Exception as e: print(f"[BluetoothManager] Error writing btaudio: {e}") @@ -125,7 +192,7 @@ def __init__(self, session): self.cb_mac_address = None self.cb_name = None - self.hasBTAudio = isfile("/proc/stb/audio/btaudio") + self.hasBTAudio = isfile(BTAUDIO_PROC) or SOFT_BTAUDIO self["audioActions"].setEnabled(self.hasBTAudio) self.rootDir = "/var/lib/bluetooth" self.controlerPath = None @@ -310,11 +377,13 @@ def keyBlue(self): if config.btdevicesmanager.audioaddress.value == current[1]: config.btdevicesmanager.audioaddress.value = "" config.btdevicesmanager.audioconnect.value = False - config.av.btaudio.value = False + if not SOFT_BTAUDIO: + config.av.btaudio.value = False else: config.btdevicesmanager.audioaddress.value = current[1] config.btdevicesmanager.audioconnect.value = True - config.av.btaudio.value = True + if not SOFT_BTAUDIO: + config.av.btaudio.value = True applyBTAudioState() self.selectionChanged() @@ -371,14 +440,21 @@ def flush(self): def sessionstart(session, reason, **kwargs): global iBluetoothDevicesTask if reason == 0: - if isfile("/proc/stb/audio/btaudio"): + if SOFT_BTAUDIO: + # Stay on HDMI until the stored audio device is connected again. + setSoftBTAudioOutput(False) + if config.btdevicesmanager.audioconnect.value: + applyBTAudioState() + elif isfile(BTAUDIO_PROC): applyBTAudioState() if iBluetoothDevicesTask is None: iBluetoothDevicesTask = BluetoothDevicesTask(session) def Plugins(**kwargs): - if fileCheck("/sys/class/bluetooth/hci0"): + # The internal UART controller of the soft BT audio boxes is attached + # in the background and may show up after the plugins are loaded. + if fileCheck("/sys/class/bluetooth/hci0") or SOFT_BTAUDIO: return [ PluginDescriptor(where=[PluginDescriptor.WHERE_SESSIONSTART], fnc=sessionstart), PluginDescriptor(name=_("Bluetooth Devices Manager"), description=_("This is bt devices manager"), icon="plugin.png", where=PluginDescriptor.WHERE_PLUGINMENU, fnc=main)