Headset Bluetooth en Debian Jessie (y Stretch)

Echar a andar un headset1 Bluetooth es uno de los mayores dolores de cabeza que se pueden encontrar actualmente en una distribución Linux, como queda demostrado si se echa un vistazo a la cantidad de mensajes en foros solicitando ayuda o al tamaño de la sección dedicada a ello del wiki de Arch Linux2. Y ahora yo también puedo decirlo de primera mano ya que lo he sufrido en mis carnes.

Hay que decir que el soporte de Bluetooth en Linux siempre ha sido un poco "magia negra". Si hace poco me quejaba de la escasa documentación de PulseAudio, el caso de la pila de Bluetooth es mucho peor si cabe. Y para ahondar aún más en el problema, una buena parte de poca información que hay por ahí ha quedado desfasada, como le pasa a la página del wiki de Debian sobre Bluetooth.

Así que, para contar esta historia tengo que empezar explicando que el soporte de Bluetooth en Linux se compone de dos partes:

  • espacio kernel: en los fuentes del kernel se implementan los drivers de acceso al hardware (a los diferentes chipsets como los de Broadcom o Atheros) y también algunos protocolos de bajo nivel. El kernel exporta una serie de dispositivos hci0, hci1, hci2, ... muy parecidos a los dispositivos eth0, eth1, ... que exportan los dispositivos de red.

  • espacio de usuario: el resto de los protocolos de alto nivel, así como las herramientas de configuración y demás las implementa BlueZ.

No voy a contar aquí cómo funciona Bluetooth (para eso está la Wikipedia), pero sí que haré un símil para que se entienda fácilmente. Así como la pila de red está formada por un montón de protocolos, desde protocolos de nivel 1 y 2 que se implementan normalmente en el propio hardware —la tarjeta de red— hasta protocolos de alto nivel como SMTP, HTTP y demás, Bluetooth también está formado por una serie de protocolos unos encima de otros. Al fin y al cabo, Bluetooth no es más que otro tipo de red, sólo que una especialmente diseñada para distancias cortas, y donde el consumo energético es una importante restricción.

En Bluetooth hay una serie de protocolos de bajo nivel —que no nos interesan en esta descripción— y luego están el equivalente a los protocolos de alto nivel —a los SMTP y HTTP de la pila de red— que en Bluetooth se llaman perfiles (profiles), y que son los que permiten que un dispositivo funcione para una tarea u otra. En este artículo mencionaré perfiles como A2DP o HSP, ambos relacionados con el audio (que es lo que nos incumbe aquí), pero hay muchos otros. También quiero señalar que cuando un dispositivo Bluetooth es algo simple como unos cascos o un ratón, estos protocolos se implementan en hardware o firmware, por lo que no son actualizables y en concreto los perfiles estarán limitados a lo que venga de fábrica.

En cambio cuando tenemos un aparato complejo, como un ordenador o un smartphone, uno se puede permitir implementar una parte de la pila de Bluetooth en software en vez de en hardware, y esta parte normalmente son los perfiles (o sea, los protocolos de alto nivel). Esa es la parte de la que esencialmente se encarga BlueZ, por sí mismo o con ayuda de otras partes del sistema operativo.

En el caso de unos auriculares o un micrófono Bluetooth —o ambos como es el caso de un headset— BlueZ colabora con el subsistema de sonido (ALSA, PulseAudio, ...) para traernos audio hasta nuestras orejas (o llevar nuestra voz).

Bluez 5 y PulseAudio 5.0 y 6.0

Cuando en Navidades de 2012 salió BlueZ 5 se produjeron un par de cambios relevantes en este software. El primero fue que BlueZ 5 eliminó el soporte para ALSA y empezó a utilizar exclusivamente PulseAudio como subsistema al que derivar las tareas de audio. Los usuarios de BlueZ que hasta ese momento habían usado ALSA no tenían más remedio que migrar a PulseAudio o quedarse anclados en BlueZ 4. Por otro lado, en BlueZ 5 los perfiles HSP (para headsets) y HFP (para manos libres telefónicos), que hasta ese momento se implementaban internamente fueron eliminados en favor de una implementación externa (concretamente por el demonio de PulseAudio uno y el software telefónico oFono el otro):

Remove internal support for telephony (HFP and HSP) profiles. They should be implemented using the new Profile interface preferably by the telephony subsystem of choice (e.g. oFono which already supports this)

Por si fueran pocos cambios, BlueZ 5 introdujo el uso de una nueva herramienta de configuración, bluetoothctl, en vez de la variada colección que había que usar en las versiones previas. Toda la documentación que había hasta el momento en foros y wikis para configurar BlueZ quedó obsoleta de un plumazo3. Es lo que le pasa a la mencionada página de Debian, por ejemplo.

Al mover BlueZ parte de su funcionalidad a PulseAudio, había que actualizar también éste. Pero hasta Marzo de 2014 no aparece la primera versión de PulseAudio con soporte para BlueZ 5, la 5.0, que sólo trae soporte para el perfil A2DP:

PulseAudio now supports BlueZ 5, but only the A2DP profile, which means that in case of headsets, only playback is available, the headset microphone can't be used. BlueZ 4 is still the only way to make the headset and hands-free profiles work.

No es hasta PulseAudio 6.0 aparecida en Febrero de 2015 que no se añade soporte para los perfiles HSP y HFP (y éste último exclusivamente mediante oFono). Esto significa que si usas la combinación BlueZ 5 junto con PulseAudio 5.0 sólo tienes soporte para la parte de auriculares de tu headset y no para el micrófono, mientras que si usas BlueZ 5 junto con PulseAudio 6.0 tienes —o deberías tener— soporte para ambos.

Me diréis que no tiene sentido usar PulseAudio 5.0 teniendo la versión 6.0 disponible, pero es que Debian Jessie lleva PulseAudio 5.0, ya que cuando apareció la versión 6.0 de PulseAudio la distribución estaba en un estado de freeze profundo y ya no se podía actualizar el paquete. Debian Stretch, la actual testing, sí que lleva PulseAudio 6.0, aunque ya veremos que eso no significa que se terminen los problemas.

Configurando el kernel para Bluetooth

Ahora que ya hemos visto el panorama general es hora que pasemos a la parte práctica y a los detalles sucios. Para los que no se quieren manchar las manos hay asistentes gráficos tipo Blueman o Bluedevil, aunque ya os adelanto que si hay problemas no os van a servir para solucionarlos si no entendéis lo que está pasando por debajo.

Lo primero que tuve que hacer en mi caso es compilar un kernel con soporte para Bluetooth. Los que uséis los kernels estándar de vuestra distribución es de suponer que ya vienen con todo compilado, incluso con aquello que nunca usaréis.

Menú de Bluetooth de KConfig

El soporte para BNEP es para hacer tethering, así que no lo he incluído. El HIDP sí, por si alguna vez uso un teclado o ratón Bluetooth (en realidad es para enlazar con la parte de dispositivos de entrada HID). Le he puesto además soporte para LE porque mi dongle soporta Bluetooth 4.0.

La opción Bluetooth device drivers da acceso al submenú donde escoger qué drivers (módulos) incluir:

Menú de Bluetooth device drivers de KConfig

Los dongles USB para equipos de sobremesa y los integrados en los laptops suelen utilizar el módulo btusb, que es el HCI USB driver en este caso, pero algunos portátiles usan otros módulos dependiendo el hardware que lleven. En mi caso comprobé qué hardware tenía, y si tenía soporte, consultando los datos USB del dongle:

$ lsusb
...
Bus 003 Device 001: ID 0a5c:21e8 Broadcom Corp. BCM20702A0 Bluetooth 4.0
...

Con el VendorID:ProdID (0a5c:21e8) me fue fácil encontrar que se había añadido soporte para este dispositivo en el módulo de btusb desde el kernel 3.4 4. Pero resulta que en mi fichero drivers/bluetooth/btusb.c del kernel 4.0.5 no encontraba los números en cuestión. No fue hasta que encontré este commit (github) que me dí cuenta de lo que había pasado: para no tener que añadir cada ProdID nuevo a la tabla de identificadores a partir de ese cambio todos los dispositivos Broadcom (VendorID=0a5c) se tratan como uno solo.

El nuevo kernel con el soporte de Bluetooth compilado y el driver btusb se veía así en dmsg tras arrancar el sistema operativo:

[   39.005283] Bluetooth: Core ver 2.20
[   39.005293] NET: Registered protocol family 31
[   39.005295] Bluetooth: HCI device and connection manager initialized
[   39.005297] Bluetooth: HCI socket layer initialized
[   39.005299] Bluetooth: L2CAP socket layer initialized
[   39.005303] Bluetooth: SCO socket layer initialized

Y enchufando el USB dongle:

[ 2996.695737] usb 3-1: new full-speed USB device number 2 using xhci_hcd
[ 2996.826020] usb 3-1: New USB device found, idVendor=0a5c, idProduct=21e8
[ 2996.826026] usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 2996.826029] usb 3-1: Product: BCM20702A0
[ 2996.826031] usb 3-1: Manufacturer: Broadcom Corp
[ 2996.826033] usb 3-1: SerialNumber: ------------
[ 2996.869091] usbcore: registered new interface driver btusb
[ 2996.869309] bluetooth hci0: Direct firmware load for brcm/BCM20702A0-0a5c-21e8.hcd failed with error -2
[ 2996.869313] Bluetooth: hci0: BCM: patch brcm/BCM20702A0-0a5c-21e8.hcd not found

Las dos últimas líneas me preocupaban, así que estuve investigando y llegué al siguiente commit "btusb: Add Broadcom patch RAM support" (github):

After hardware reset, some BCM Bluetooth adapters obtain their initial firmware from OTPROM chip. Once this initial firmware is running, the firmware can be further upgraded over HCI interface with .hcd files provided by Broadcom. This is also known as "patch RAM" support. This change implements that.

If the .hcd file is not found in /lib/firmware, BCM Bluetooth adapter continues to operate with the initial firmware.

Antes he dicho que los dispositivos de capacidad reducida (como es el caso de este dongle) tienden a implementar Bluetooth en hardware o firmware, por lo que luego no se pueden actualizar. Parece que Broadcom ha intentado superar esta limitación haciendo que el driver que lo controla le mande al dispositivo una versión más moderna del firmware cuando lo inicializa. Esos firmwares más modernos son proporcionados por el fabricante en forma de ficheros (en el caso de Broadcom con extensión .hcd) para que el driver los lea y envíe.

El fichero de firmware que buscaba el driver para mi hardware (brcm/BCM20702A0-0a5c-21e8.hcd) no aparecía ni en el paquete firmware-linux-free (esperable dada la actitud de Broadcom hacia el software libre) ni en el más general repositorio git de firmwares en kernel.org (menos esperable) que contiene firmwares no libres pero sí con permiso de distribución. Tampoco aparecía ninguna referencia a drivers para Linux en el CD que viene con el dongle.

Lo que era más preocupante: buscando por ese nombre de fichero en la Web, encontré que se necesitaba el firmware de marras para hacer funcionar los perfiles HSP y HFP. Pero como en ese momento estaba en Debian Jessie con PulseAudio 5.0 (que ya he dicho que carece de soporte para estos perfiles) me dije que de momento podía probar a ver si funcionaba sin el fichero de firmware. Aunque este otro comentario en la lista de correo de linux-bluetooth:

Anyhow, one minor thing that bugs me is this:

[193452.790194] Bluetooth: hci0: BCM: patch brcm/BCM20702A0-0a5c-21e8.hcd not found

While I want the firmware loading available for all Broadcom controllers, I do think we might have to mark some of them as firmware absolutely required since otherwise they will not work. And in that case we should print the error. Otherwise we might just silently not print anything. Thoughts?

parecía dar a entender que en el caso de mi dongle el firmware era imprescindible para que funcionara correctamente. De hecho, lo que en ese hilo se sugiere es implementar que, en los casos en que se sabe positivamente que el firmware es necesario, si no se encuentra el fichero correspondiente el driver emita un error y no inicialice el dispositivo. Pero desde que se había escrito ese mensaje (8 de mayo de 2014) no se había hecho nada en esa línea, y el driver seguía simplemente emitiendo un aviso cuando no encontraba el fichero de firmware (como en mi caso).

A pesar de los malos augurios, me lancé primero a intentar hacer funcionar el headset sin firmwares binarios propietarios opacos.

Configurando Bluetooth en el espacio de usuario

Tener el kernel con el driver cargado es sólo la mitad del trabajo. Ahora es cuando BlueZ, la parte de espacio de usuario, entra en escena. Lo primero es instalarlo, e instalar también el módulo de integración con PulseAudio:

# aptitude install bluez pulseaudio-module-bluetooth
Se instalarán los siguiente paquetes NUEVOS:     
  bluez libsbc1{a} pulseaudio-module-bluetooth

La versión de BlueZ que lleva Debian Jessie es la 5.23, que es la misma que de momento lleva Stretch, aunque en el instante que escribo esto la versión más reciente de upstream es la 5.31.

La parte fundamental de BlueZ es bluetoothd, un demonio del sistema que es el que se encarga de gestionar los interfaces Bluetooth existentes, y de hablar con los dispositivos que se pongan dentro de su alcance para presentarlos a otros servicios según qué perfiles soporten5. Por ejemplo, en mi caso quiero que si un dispositivo Bluetooth —mi headset— entra en el alcance de mi dongle, bluetoothd interrogue al headset para descubrir que soporta los perfiles A2DP y/o HSP, y a partir de ello informe a PulseAudio que hay disponibles dos nuevos dispositivos: los auriculares a los que enviar audio y el micrófono del que recibirlo.

Por lo tanto es obvio que si queremos que los dispositivos Bluetooth funcionen, debemos asegurarnos que bluetoothd esté ejecutándose de contínuo. En Debian, el demonio lo tiene que haber arrancado el sistema de init que uséis (System V o systemd), y el servicio debe estar en marcha. Por ejemplo, en systemd:

# systemctl status bluetooth.service 
● bluetooth.service - Bluetooth service
   Loaded: loaded (/lib/systemd/system/bluetooth.service; enabled)
   Active: active (running) since mar 2015-05-30 18:43:12 CEST; 36min ago
     Docs: man:bluetoothd(8)
 Main PID: 3731 (bluetoothd)
   Status: "Running"
   CGroup: /system.slice/bluetooth.service
           └─3731 /usr/lib/bluetooth/bluetoothd

may 30 18:43:12 ******** bluetoothd[3731]: Bluetooth daemon 5.23
may 30 18:43:12 ******** bluetoothd[3731]: Starting SDP server
may 30 18:43:12 ******** bluetoothd[3731]: kernel lacks bnep-protocol support
may 30 18:43:12 ******** bluetoothd[3731]: System does not support network plugin
may 30 18:43:12 ******** bluetoothd[3731]: Failed to open RFKILL control device
may 30 18:43:12 ******** bluetoothd[3731]: Bluetooth management interface 1.9 initialized

Si usáis System V init los mensajes de log estarán en /var/log/daemon.log. Dos aclaraciones con respecto a lo que pueden parecer errores:

  1. Recordad que quité el soporte de BNEP de la configuración de mi kernel (por eso tampoco tengo soporte del plugin de network)
  2. Un dongle enchufado a un USB evidentemente no tiene botones para encender/apagar el Bluetooth como existen en los portátiles. Por eso no hay un dispositivo RFKILL que abrir (vamos, que no es un error).

Es hora de conectar el dongle a un puerto USB y ver qué pasa:

bluetoothd[3731]: Sap driver initialization failed.
bluetoothd[3731]: sap-server: Operation not permitted (1)

Olvidad de momento estos errores que aparecen en los logs (más adelante explicaré por qué aparecen y cómo quitarlos) y centrémonos en ver si el dispositivo Bluetooth está en marcha. Para ello usaremos una herramienta de bajo nivel de las que vienen con BlueZ: hciconfig. Esta herramienta es el equivalente a ifconfig pero para dispositivos HCI Bluetooth:

$ hciconfig
hci0:   Type: BR/EDR  Bus: USB
    BD Address: XX:XX:XX:XX:XX:XX  ACL MTU: 1021:8  SCO MTU: 64:1
    UP RUNNING 
    RX bytes:1504 acl:0 sco:0 events:180 errors:0
    TX bytes:29730 acl:0 sco:0 commands:179 errors:0

Como podéis observar, ahora tengo un dispositivo hci0, así que el dongle ha sido reconocido por el kernel y está funcionando. El dongle tiene una dirección de red única similar a la de las tarjetas de red Ethernet6 que yo he preferido ocultar, por lo que aparece como XX:XX:XX:XX:XX:XX.

Aunque hemos usado hciconfig para ver que el dispositivo está ahí, no lo vamos a emplear para configurarlo como se hacía antiguamente. En BlueZ 5 es el demonio bluetoothd el que controla los dispositivos HCI (hci0, hci1, etc). Los usuarios le dan órdenes a bluetoothd mediante la herramienta bluetoothctl y éste hace lo que haga falta con el dispositivo en cuestión, almacenando el estado si fuera necesario:

$ bluetoothctl
[NEW] Controller XX:XX:XX:XX:XX:XX ******** [default]
[bluetooth]#

El prompt de bluetoothctl está esperando comandos que pasarle a bluetoothd. help os dará una lista de los comandos disponibles. Por ejemplo, si hciconfig hubiera mostrado el dispositivo como DOWN en vez de UP RUNNING, podríamos ordenar levantarlo con un power on. En mi caso ya está levantado, así que no es necesario a pesar de lo que diga el Arch Wiki. Los siguientes comandos de esa lista en cambio sí son necesarios, aunque es suficiente con ejecutarlos una única vez (la primera), ya que bluetoothd guarda su estado y los recordará:

[bluetooth]# agent on
Agent registered
[bluetooth]# default-agent
Default agent request successful

Ahora el dongle está listo, pero todavía no está buscando activamente a otros dispositivos Bluetooth que pueda haber en su radio de alcance. Estar activo significa gastar energía, y en Bluetooth el ahorro de energía es una prioridad. Así que debemos decirle explícitamente al dispositivo HCI que active la búsqueda de otros dispositivos Bluetooth:

bluetooth]# scan on
Discovery started
[CHG] Controller XX:XX:XX:XX:XX:XX Discovering: yes
[bluetooth]#

En este momento enciendo el headset y espero hasta que me reconoce el nuevo dispositivo a través de la conexión Bluetooth (paciencia, le cuesta un poco). Cuando aparece:

[NEW] Device YY:YY:YY:YY:YY:YY Energy BT5+
[bluetooth]#

es que el dongle ha encontrado un nuevo dispositivo —el headset—, y además nos dice la dirección de red de éste (que yo he ocultado bajo YY:YY:YY:YY:YY:YY).

Por motivos de seguridad, no es buena idea permitir que cualquier dispositivo sin identificar establezca una conexión sin más con nuestro sistema. Imaginaos el cachondeo que sería que cualquiera con un teclado Bluetooth pudiera conectarlo a nuestro ordenador desde la habitación de al lado y teclear lo que le diera la gana como si fuéramos nosotros7. El usuario tiene de alguna forma que dar el visto bueno al dispositivo remoto que se quiere conectar, y esto se hace mediante el emparejado (pair) de los dos dispositivos:

[bluetooth]# pair YY:YY:YY:YY:YY:YY
Attempting to pair with YY:YY:YY:YY:YY:YY
[CHG] Device YY:YY:YY:YY:YY:YY Connected: yes
[CHG] Device YY:YY:YY:YY:YY:YY UUIDs:
    00001108-0000-1000-8000-00805f9b34fb
    0000110b-0000-1000-8000-00805f9b34fb
    0000110c-0000-1000-8000-00805f9b34fb
    0000110e-0000-1000-8000-00805f9b34fb
    0000111e-0000-1000-8000-00805f9b34fb
    00001200-0000-1000-8000-00805f9b34fb
[CHG] Device YY:YY:YY:YY:YY:YY Paired: yes
Pairing successful
[CHG] Device YY:YY:YY:YY:YY:YY Connected: no

El emparejado ha funcionado, aunque luego el dispositivo se ha desconectado (puede ser simplemente por el ahorro de energía).

Como no queremos estar emparejando manualmente el headset cada vez que lo encendamos, le vamos a indicar a bluetoothd que en adelante cuando vuelva a encontrar al dispositivo YY:YY:YY:YY:YY:YY se fíe de él y haga el pair automáticamente sin que nosotros le digamos nada. Esto se consigue mediante el comando trust:

bluetooth]# trust YY:YY:YY:YY:YY:YY
[CHG] Device YY:YY:YY:YY:YY:YY Trusted: yes
Changing YY:YY:YY:YY:YY:YY trust succeeded

Estoy diciendo que bluetoothd guarda toda esta información, pero es mejor que lo veáis por vosotros mismos: está en /var/lib/bluetooth/ (acceso como root). Por cierto, si por cualquier causa queréis purgar el paquete bluez para empezar desde cero, tendréis que borrar manualmente el contenido de ese directorio porque el paquete Debian no lo hace cuando se desinstala (Bug #511608).

Ahora es cuando finalmente conectamos con el headset:

[bluetooth]# connect YY:YY:YY:YY:YY:YY
Attempting to connect to YY:YY:YY:YY:YY:YY
Failed to connect: org.bluez.Error.Failed

¡¡Error!! Tranquilos, está todo bajo control. Quería mostraros el que creo que es el punto en el que mucha gente se atasca. Veamos lo que dicen los logs:

may 30 19:32:04 ******** bluetoothd[3731]: a2dp-sink profile connect failed for YY:YY:YY:YY:YY:YY: Protocol not available

Lo de "a2dp-sink" es una buena pista de la causa del problema. ¿Os acordáis que cuando instalé el paquete bluez instalé también pulseaudio-module-bluetooth? Pues lo que pasa es que el módulo en cuestión no está cargado en el proceso de PulseAudio que ya está lanzado. Podríamos parar y reiniciar el servicio para que lea la nueva configuración, pero me cuesta menos decirle a PulseAudio que cargue el módulo:

$ pactl load-module module-bluetooth-discover 
20
$ pactl list short modules | grep bluetooth
20  module-bluetooth-discover

¿Alguién más piensa como yo que el paquete pulseaudio-module-bluetooth debería encargarse de ello automáticamente si el servicio PulseAudio está ya en marcha?

Volviendo a los logs, ahora muestran una cosa bien distinta:

may 30 19:41:29 ******** bluetoothd[3731]: Endpoint registered: sender=:1.42 path=/MediaEndpoint/A2DPSource
may 30 19:41:29 ******** bluetoothd[3731]: Endpoint registered: sender=:1.42 path=/MediaEndpoint/A2DPSink

El demonio bluetoothd ha sido capaz de registrar dos objetos D-Bus, uno para los auriculares del headset (/MediaEndpoint/A2DPSink) y otro para el micrófono (/MediaEndpoint/A2DPSource), que son los que empleará PulseAudio para enviar/recibir audio. Esto sucede en cuanto se enchufa el dongle, no hace falta que esté encendido el headset o cualquier otro dispositivo con perfiles A2DP y/o HSP.

Si encendemos ahora el headset y entramos en bluetoothctl veremos que bluetoothd ha establecido automáticamente la conexión sin que le hayamos tenido que decir nada (si no fuera así, ejecutad el comando connect YY:YY:YY:YY:YY:YY manualmente, ahora no debería dar error; también puede ser un buen momento para parar el scan con un scan off si no tenemos más dispositivos que emparejar). Y en los logs aparece:

may 30 19:44:41 ******** bluetoothd[3731]: /org/bluez/hci0/dev_YY_YY_YY_YY_YY_YY/fd0: fd(18) ready
may 30 19:44:41 ******** bluetoothd[3731]: Can't open input device: No such file or directory (2)
may 30 19:44:41 ******** bluetoothd[3731]: AVRCP: failed to init uinput for YY:YY:YY:YY:YY:YY

La primera línea (otro objeto exportado a través de D-Bus) nos confirma que la conexión con el pair remoto se ha establecido. Para las otras dos líneas de error he encontrado una solución en el wiki de Gentoo: compilar el kernel con estas opciones:

Device Drivers  --->
   Input device support  --->
      [*]   Miscellaneous devices  --->
         <M>   User level driver support

/dev/uinput es un driver para crear dispositivos de entrada virtuales en /dev/input/ desde espacio de usuario. bluetoothd crea un nuevo dispositivo de entrada /dev/input/eventN (siendo N un número correlativo) cuando enciendo el headset, y en los logs del kernel aparece:

input: YY:YY:YY:YY:YY:YY as /devices/virtual/input/input17

AVRCP es un perfil para mandos a distancia de video y de audio, pero ignoro exactamente cuál es el propósito de este dispositivo de input en ese contexto.

En busca del firmware perdido

Me había costado, pero tras hacer todo lo anterior por fin había conseguido que funcionara el headset en Debian Jessie. Y colorín colorado, este cuento se ha acabado. Excepto que no se ha acabado, porque como no me puedo quedar quieto, hace un par de semanas salté a Stretch, con el desastroso resultado de que el headset me dejó de funcionar. :-(

He dicho "me dejó de funcionar" pero en realidad lo que pasaba era lo siguiente: cuando se enviaba audio a PulseAudio y el headset estaba encendido, la aplicación que enviaba el audio (exaile, mplayer, etc) se quedaba "clavada" (la reproducción del audio —y del video también en el caso de mplayer— no avanzaba). En el caso de exaile, si intentaba parar la canción la aplicación se quedaba absolutamente congelada. Si apagaba el headset (lo que conmutaba el audio al sink de ALSA), las aplicaciones volvían a la vida y el sonido empezaba a reproducirse normalmente.

Como las diferencias entre Debian Jessie y Debian Stretch son todavía pequeñas, y el cambio fundamental es que Stretch había actualizado PulseAudio de la 5.0 a la 6.0, lo que hice fue probar a hacer un downgrade de los 7 paquetes de PulseAudio que tenía instalados, sustituyendo las versiones 6.0-2 por las 5.0-13 de Jessie.8 El resto de paquetes eran los de Stretch, incluyendo BlueZ (que en este caso da igual porque todavía es exactamente el mismo que el de Jessie). Con los paquetes 5.0-13 el headset volvía a funcionar sin problemas, lo que me permitía acotar el problema a los cambios que había introducido PulseAudio 6.0.

¿Y qué era lo más significativo que había introducido PulseAudio 6.0? Si habéis seguido atentamente las explicaciones del principio ya sabréis la respuesta: el soporte de perfiles HSP y HFP, ¡por algo solté todo el rollo! Y hay otro momento en esta historia en que también se mencionan dichos perfiles: cuando hablé sobre si necesitaba o no cargar el firmware propietario de Broadcom en el driver, y de los bugs que supuestamente éste arreglaba (relacionados con HSP/HFP). Llegados a este punto, el sentido común dictaba que era conveniente probar a cargar el firmware a ver si arreglaba algo.

Sólo restaba encontrar el firmware adecuado, lo que ha sido una pequeña odisea en sí misma. En la página del vendedor de dongles que afirmaba que se necesitaba el firmware para hacer funcionar los perfiles HSP/HFP (que no es la marca de mi dongle pero que debe ser un revendedor análogo porque es clavadito al mío) ofrecía para descargar el siguiente fichero:

-rw-r--r-- 1 ****** ****** 34700 feb 24  2014 fw-0a5c_21e8.hcd

Buscando por el mismo nombre de fichero, encontré otra versión que tenía un tamaño diferente (28758 bytes) lo que me dejaba la duda de cuál era el más reciente. Tampoco me hacía mucha gracia usar un blob opaco descargado de un sitio random de Internet, pero en la página oficial de Broadcom no hay disponible un driver para descargar del que poder extraerlo. Lo más que hay es un ejecutable para detectar qué dispositivo Bluetooth tienes y aparentemente descargarse el driver adecuado, pero como era previsible no funciona con wine (no es capaz de pasar de la fase de detección):

La aplicación de Broadcom ejecutándose en wine

(Tampoco funcionó el truco de sacar los strings del ejecutable a ver si entre ellos aparecía la URL desde la que se descargaban los drivers).

Una alternativa que encontré era seguir estas instrucciones para obtener un .hcd a partir de los drivers de Windows. En éstos existen una serie de ficheros .hex (que parecen simplemente una versión hexadecimal del firmware) a partir de los cuales se puede generar el .hcd correspondiente usando la herramienta hex2hcd. BlueZ trae esta herramienta (que por cierto en la versión 5.28 ha sido reescrita) pero Debian a día de hoy no la empaqueta (Bug 780015) por lo que si queremos usarla hay que bajarse el fichero hex2hcd.c y compilarlo a mano.

El problema de este método alternativo es encontrar cuál es el .hex adecuado, porque hay decenas de ficheros de este tipo en los drivers de Windows. Hay que buscar la cadena VendorID y ProdID (en mi caso 0A5C:21E8) en los distintos ficheros *.INF hasta dar con la sección relevante, donde aparecerá algún nombre de fichero como éste: BCM20702A1_001.002.014.0187.0188.hex. Ese fichero (de tamaño 43432 bytes) es concretamente el que encontré en el CD de drivers que acompañaba a mi dongle. Para añadir más confusión, el .hcd generado a partir del mismo tenía un tamaño distinto al de los otros:

-rw-r--r-- 1 ****** ****** 21756 jun 24 16:43 fw-0a5c_21e8-BCM20702A1_001.002.014.0187.0188.hcd
-rw-r--r-- 1 ****** ****** 34700 feb 24  2014 fw-0a5c_21e8.hcd
-rw-r--r-- 1 ****** ****** 28758 jun 24 16:45 otro-fw-0a5c_21e8.hcd

De todas formas, el fichero .hex extraido del CD de drivers era de 2011, por lo que tenía pinta de ser una versión más antigua del firmware.

Al final me encontré en una página de soporte de Lenovo unos drivers Broadcom para Windows de 2013, y en ellos un fichero llamado BCM20702A1_001.002.014.0889.0896.hex de 57399 bytes, que convertido a .hcd me daba un fichero exactamente igual que el firmware de 28758 bytes que ya me había bajado antes. Esta es la versión que estoy usando:

usb 3-1.6: new full-speed USB device number 5 using ehci-pci
usb 3-1.6: New USB device found, idVendor=0a5c, idProduct=21e8
usb 3-1.6: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 3-1.6: Product: BCM20702A0
usb 3-1.6: Manufacturer: Broadcom Corp
usb 3-1.6: SerialNumber: ------------
Bluetooth: hci0: BCM: chip id 63
Bluetooth: hci0: BCM20702A1 (001.002.014) build 0000
Bluetooth: hci0: BCM20702A1 (001.002.014) build 0896

La última línea es la del "patch RAM", y ya véis su semejanza al nombre del fichero .hex utilizado para generarlo.

Lo que no he podido encontrar es un .hex más moderno de un driver más reciente que se corresponda con el .hcd de 34700 bytes.

Perfiles, perfiles everywhere

Una vez puesto el firmware en el path en el que lo busca el driver del kernel, enciendo el headset y el sonido se escucha ¡Por fin! Pero es un sonido de una calidad bajísima y con mucha "fritura"9.

Después de estar un buen rato cavilando qué estaba mal (le estaba echando la culpa a la versión del firmware), por fin caigo: esa debe ser la calidad para perfiles HSP/HFP. Lo único que tengo que hacer es cambiar el perfil:

$ pactl list cards
Card #3
    Name: bluez_card.YY_YY_YY_YY_YY_YY
    Driver: module-bluez5-device.c
    Owner Module: 28
    Properties:
        device.description = "Energy BT5+"
        device.string = "YY:YY:YY:YY:YY:YY"
        device.api = "bluez"
        device.class = "sound"
        device.bus = "bluetooth"
        device.form_factor = "headset"
        bluez.path = "/org/bluez/hci0/dev_YY_YY_YY_YY_YY_YY"
        bluez.class = "0x240404"
        bluez.alias = "Energy BT5+"
        device.icon_name = "audio-headset-bluetooth"
        device.intended_roles = "phone"
    Profiles:
        headset_head_unit: Headset Head Unit (HSP/HFP) (sinks: 1, sources: 1, priority: 20, available: yes)
        a2dp_sink: High Fidelity Playback (A2DP Sink) (sinks: 1, sources: 0, priority: 10, available: yes)
        off: Apagado (sinks: 0, sources: 0, priority: 0, available: yes)
    Active Profile: headset_head_unit
    Ports:
        headset-output: Headset (priority: 0, latency offset: 0 usec)
            Part of profile(s): headset_head_unit, a2dp_sink
        headset-input: Headset (priority: 0, latency offset: 0 usec)
            Part of profile(s): headset_head_unit

La sección que nos interesa es la de "Profiles:". Véis que me está detectando dos perfiles (tres si contáis el perfil "off"). Pero el perfil correspondiente al HSP/HFP tiene más prioridad (20) que el de A2DP (10), así que se selecciona por defecto si no le decimos nada. Para cambiar el perfil usamos el siguiente comando, indicando tarjeta y perfil a activar:

$ pactl set-card-profile 3 a2dp_sink

Si no queremos indicar el número de tarjeta (que cambia cada vez que se enciende y apaga el headset) podemos usar el nombre (que permanece constante pero es más largo):

$ pactl set-card-profile bluez_card.YY_YY_YY_YY_YY_YY a2dp_sink

El perfil naturalmente tiene que estar disponible (available: yes) o nos devolverá un error bastante críptico (algo así como "Error de Entrada/Salida").

Si todo ha ido bien, ahora el estado de la tarjeta que corresponde al headset tendrá el nuevo perfil activo (Active Profile):

$ pactl list cards
[ ... ]
Profiles:
    headset_head_unit: Headset Head Unit (HSP/HFP) (sinks: 1, sources: 1, priority: 20, available: yes)
    a2dp_sink: High Fidelity Playback (A2DP Sink) (sinks: 1, sources: 0, priority: 10, available: yes)
    off: Apagado (sinks: 0, sources: 0, priority: 0, available: yes)
Active Profile: a2dp_sink

Y ahora el audio volverá a oirse con gran calidad con PulseAudio 6.0. ¡¡Hurra!!

(Sí, me podría haber ahorrado la etapa completa de la búsqueda del firmware, ya que lo único que necesitaba para que el headset volviera a funcionar en PulseAudio 6.0 era cambiar el perfil a a2dp_sink. Los bugs que corrige el firmware parece que se limitan al perfil HSP/HFP en este caso —o tal vez no, pero no he notado la diferencia—. Lo único que puedo decir es que es fácil hablar a toro pasado :-P).

PulseAudio recuerda el perfil activo de cada tarjeta, así que no hace falta estar seleccionandolo en cada arranque del demonio. Si lo que queremos es volver a pasar al perfil HSP/HFP (por ejemplo para usar el micrófono) hay que volver a cambiar el perfil activo:

$ pactl set-card-profile 3 headset_head_unit

Observad que a2dp_sink tiene 1 sink y 0 sources, mientras que headset_head_unit tiene 1 sink y 1 source. Los sinks (sumideros) corresponden en terminología PulseAudio a las salidas de audio, mientras que las sources (fuentes) corresponden a las entradas. El perfil de alta fidelidad A2DP no tiene entradas, por lo que no se puede usar el micrófono con este perfil. Si queremos usar la entrada de audio no nos queda más remedio que seleccionar el perfil HSP/HFP.10

Hay otra forma más visual de escoger el perfil usando la pestaña "Configuración" de pavucontrol (PulseAudio Volume Control). Aquí he preferido usar pactl para mostraros detalles de bajo nivel (como las prioridades o la disponibilidad) que son útiles si hay problemas. pavucontrol puede ser más atractivo, pero sólo nos permite seleccionar el perfil sin darnos información adicional:

Perfiles en pavucontrol

Con tanta prueba y tanto cambio, en algún momento me ha pasado que un perfil me aparecía como no disponible:

$ pactl list cards
[ ... ]
Profiles:
    a2dp_sink: High Fidelity Playback (A2DP Sink) (sinks: 1, sources: 0, priority: 10, available: no)
    headset_head_unit: Headset Head Unit (HSP/HFP) (sinks: 1, sources: 1, priority: 20, available: yes)
    off: Apagado (sinks: 0, sources: 0, priority: 0, available: yes)

Este tipo de estados no deberían ocurrir (y mucho menos que uno aparezca como disponible y el otro no), pero al estar jugando con los firmwares y demás tampoco me extraña que de repente haya una discrepancia entre los datos de estado almacenados por bluetoothd y los de PulseAudio. Comprobad con bluetoothctl que la conexión está establecida, y si no es así tratad de establecerla con un connect YY:YY:YY:YY:YY:YY. Si la conexión no se establece es cuando hay que empezar a investigar.

Otra posibilidad de recuperación más drástica que se me ocurre es tratar que ambos demonios olviden el estado que tienen almacenado del dispositivo, y empezar de nuevo todo el proceso de configuración desde cero. bluetoothctl tiene el comando remove para borrar un dispositivo remoto, lo que nos permite reemparejarlo de nuevo, pero no he encontrado ningún método equivalente en PulseAudio. Lo único que se me ocurre es hacerlo a las bravas: parar el demonio y borrar los ficheros donde guarda el estado en .config/pulse/*, pero este método tiene el serio inconveniente de borrar el estado de todo (tarjetas, volúmenes, etc). Si alguien sabe un método mejor estaría bien conocerlo.

Últimos retoques

No se me ha olvidado que dije que iba a explicar el origen de estos mensajes de error y cómo deshacerse de ellos:

bluetoothd[3731]: Sap driver initialization failed.
bluetoothd[3731]: sap-server: Operation not permitted (1)

SAP son las siglas de SIM Access Profile, otro perfil más de Bluetooth (y ya van...). Aunque un ordenador raramente va a tener acceso a una tarjeta SIM de teléfono, BlueZ es también la pila Bluetooth usada en Android11. Así que los desarrolladores de BlueZ han incluido este perfil que permite "exportar" la SIM vía Bluetooth a otro dispositivo (sea cual sea la utilidad de algo así).

El soporte de SAP en BlueZ es experimental, pero los mantenedores del paquete en Debian han decidido compilar BlueZ con el flag --enable-experimental, y por eso SAP está incluido en la versión de bluetoothd de Jessie y Stretch y por eso aparecen esos mensajes. Lo que no sería mayor problema si no fuera porque mucha gente los confunde con el motivo por el que su dispositivo Bluetooth no funciona, mandándoles tras una pista falsa.

La solución más sencilla es pasar de estos mensajes ya que son inofensivos. Pero si nos resultan realmente irritantes, hay una manera de hacerlos desaparecer: decirle bluetoothd que desactive el plugin de SAP pasándole el parámetro --noplugin=sap en el arranque del demonio. Esto es más fácil o más difícil dependiendo del sistema de arranque que utilicéis. Por ejemplo, en systemd hay que modificar la unidad bluetooth.service para añadir el parámetro a la línea de ExecStart (solo que en systemd se supone que no hay que modificar los ficheros de unidades suministrados por los packagers sino añadir un fichero de unidad nuevo bajo /etc/systemd/system/ que toma precedencia; consultad la documentación más reciente de systemd en Debian para estar seguros).

Me dan ganas de abrir un bug en el BTS para que sean los propios Debian maintainers los que lo añadan por defecto.

Conclusiones

El protocolo Bluetooth, al intentar cubrir tantos dispositivos diferentes y tantos casos de uso, no es precisamente simple. La implementación en Linux lo empeora con una escasísima documentación y con cambios significativos a lo largo del tiempo. Si a esto le añadimos su interacción con otras piezas problemáticas como PulseAudio, y lo aderezamos todo con bugs hardware sólo solucionables mediante ficheros binarios "mágicos" difíciles de encontrar, entonces uno entiende por qué hay tantísimos problemas con algo que debería ser aparentemente más sencillo.

He dedicado mucho tiempo a poner el headset en marcha —y a escribir este artículo también—. Me podría haber ahorrado algo de trabajo si hubiera sido más espabilado, pero al final hubiera tenido que hacerlo igualmente si quería usar el micrófono. Y me queda también el consuelo de que me ha servido para terminar aprendiendo un montón de cosas de Bluetooth y PulseAudio por el camino. ;-)

:wq


  1. Se llama headset a una combinación de auriculares y micrófono, sean con cable o inalámbricos. 

  2. El Wiki de Arch Linux es considerado una de las mejores fuentes de información para configurar cualquier aspecto de Linux. 

  3. Excepto que todavía uséis una distro con BlueZ 4 o anterior. 

  4. Los commits en github tienen la pequeña ventaja de mostrar los tags en los que fueron incluidos, los del interfaz git de kernel.org no. 

  5. La magia la hace Service Discovery Protocol (SDP), que es el protocolo de Bluetooth para consultar qué perfiles soporta el dispositivo remoto. 

  6. De hecho son otorgadas por el mismo organismo y ambas comparten el espacio de direciones de 48 bits. 

  7. ¡La de bromas que íbais a gastar en la oficina! }:-P 

  8. Hay que ser cuidadoso cuando se hacen este tipo de cosas si no se quiere terminar con el sistema medio roto. 

  9. Argot de radioaficionados. 

  10. He probado el micro usando la grabación desde audacity y me funciona. 

  11. Aunque le fastidie a Google (ya que lleva licencia GPL). 

blogroll

social