Simulador de Bluetooth de baja energía: una nueva esperanza en el desarrollo del IoT

Vivimos en el siglo XXI, donde puede que los animales aún no nos hablen, pero las cosas están empezando a hacerlo. Andamos con nuestros centros de control de misión (también conocidos como smartphones) que pueden manejar nuestro dinero, nuestra comunicación, nuestras noticias, nuestras casas inteligentes y cualquier otra solución inteligente que nos rodea, en definitiva, nuestras vidas.

¿Estrenar para una carrera? Puedes hacerlo con un monitor inteligente de oxígeno muscular Humon. ¿Quieres mejorar tu calidad de sueño? Consigue una banda inteligente, un reloj inteligente o un rastreador de sueño (todos ellos utilizan Bluetooth Low Energy y se comunican con tu teléfono). ¿Quieres llegar a algún sitio en bicicleta? Utiliza la navegación con BLE para minimizar las distracciones, como las notificaciones, pero saber cómo llegar a tu destino. ¿Quizás te gustaría saber cuándo está listo el café de la oficina?

Pero con la llegada del Internet de las cosas y la creciente interconexión del mundo llegaron nuevos retos para nosotros, los desarrolladores.

Desarrollar una aplicación con BLE

Si alguna vez has trabajado en una aplicación móvil que se conecta a un dispositivo Bluetooth Low Energy, sabes que no es la tarea más fácil. Hay muchas pantallas y estados internos en la app que están conectados con el periférico y su comportamiento. A menudo, la aplicación se crea junto con el firmware (a veces incluso con el hardware) del dispositivo, la conexión puede ser inconstante, la interfaz externa inestable y la lógica interna, defectuosa. Como resultado, el equipo está perdiendo mucho tiempo en Known Issues™ al probar sus características.

En general, hay dos maneras de abordar esto:

  1. En primer lugar, hay una manera fácil, que consume tiempo: utilizar un teléfono inteligente físico y un dispositivo BLE, pasar por toda la molestia de conectar y configurar el dispositivo al estado que necesitamos que sea y recrear las condiciones de prueba.
  2. Luego está la forma difícil: abstraer el dispositivo, crear una simple maqueta que pueda ocultar el manejo real de BLE. Esto te permitirá trabajar en un emulador de Android/simulador de iOS, ahorrando algo de tiempo después y permitiéndote ejecutar pruebas automatizadas en tus CIs. Al mismo tiempo, aumenta el coste de mantenimiento e introduce un nuevo riesgo al no probar la comunicación real cada vez que se ejecuta el código. Después de todo, ese periférico Bluetooth está probablemente en el corazón de nuestra aplicación y no debe desconectarse inesperadamente o comportarse de forma extraña.

Nuestros amigos de Frontside-Consultoría de ingeniería y arquitectura de software frontend con sede en Austin- han reconocido la necesidad de una solución más eficaz. Nos pidieron que desarrolláramos una solución de código abierto para un desarrollo fiable de aplicaciones con BLE del que todo el mundo pudiera beneficiarse.

Y así llega…

BLEmulator /pronun.: bleh-mulator/, un simulador de Bluetooth Low Energy.

¡BLEmulator está aquí para hacerte la vida más fácil! Maneja todo su código de producción relacionado con BLE y simula el comportamiento de un periférico real y la pila Bluetooth del sistema. Es simple y flexible, permitiéndote crear tanto el mock básico como una simulación completa de un dispositivo con BLE. Y lo mejor de todo es que es de código abierto

Le permite probar el concepto detrás de su dispositivo sin el coste de la creación de prototipos de hardware. Permite a tu equipo de móviles avanzar sin esperar al firmware o a los prototipos, sólo con una especificación. Te permite trabajar sólo con el emulador de Android o el simulador de iOS, permitiendo así una mayor movilidad, facilitando el trabajo en remoto y evitando la limitada disponibilidad de smartphones físicos. Te permite probar tus aplicaciones en pruebas automatizadas ejecutadas por tu CI.

Actualmente BLEmulator está disponible para Flutter y funciona sólo con nuestra FlutterBleLib.

Cómo funciona

Flutter, como framework multiplataforma, necesita dependencias nativas para ambas plataformas. Normalmente, esas serían una parte de la propia biblioteca, pero aquí hemos utilizado un enfoque diferente. Polidea tiene una librería React Native BLE, llamada react-native-ble-plx, un impresionante trabajo de nuestros colegas. Hemos decidido extraer toda la lógica nativa de la misma a una librería separada, conocida como Multiplatform BLE Adapter. De esta manera hemos creado un núcleo común que es utilizado tanto por react-native-ble-plx como por nuestro plugin de Flutter, FlutterBleLib. Como efecto secundario hemos creado una abstracción común utilizada en el puente nativo, el BleAdapter, ¡que es un punto de entrada perfecto para la simulación!

Este es el aspecto del flujo de datos de FlutterBleLib:

  1. Llama a un método de uno de los objetos FlutterBleLib (BleManager, Peripheral, Service, Characteristic)
  2. El código Dart envía un nombre de método y sus parámetros a un puente nativo
  3. El puente nativo recibe los datos y los deserializa si es necesario
  4. El puente nativo llama a un método apropiado en la instancia BleAdapter del Adaptador BLE Multiplataforma
  5. BleAdapter llama al método en RxAndroidBle o RxBluetoothKit, dependiendo de la plataforma
  6. RxAndroidBle/RxBluetoothKit llama a un método del sistema
  7. El sistema devuelve una respuesta al intermediario
  8. El intermediario devuelve la respuesta al BleAdapter
  9. BleAdapter responde al puente nativo
  10. El puente nativo mapea la respuesta y la envía a Dart
  11. Dart analiza la respuesta y la devuelve al llamante original

Teníamos el trabajo hecho: los puntos 4 a 9 son los puntos de entrada ideales con un contrato externo establecido. Inyectando una implementación diferente del BleAdapter se puede encender la simulación cuando queramos.

A continuación, teníamos que decidir dónde debía tener lugar la simulación. Optamos por mantener la mayor parte posible de la simulación en Dart, por dos razones principales:

  1. Una definición para ambas plataformas

Es importante minimizar tanto el número de lugares en los que es posible cometer un error como la cantidad de trabajo necesaria para crear un periférico simulado y mantenerlo.

  1. Lenguaje nativo de la plataforma

Esto tiene unas cuantas ventajas. En primer lugar, los desarrolladores trabajarán más eficientemente con una herramienta conocida, por lo que deberíamos evitar introducir lenguajes adicionales. En segundo lugar, no queríamos limitar las cosas que son posibles de hacer en el periférico simulado. Si quieres solicitar una respuesta de algunos servidores HTTP (¿tal vez con una simulación más avanzada, ejecutando el propio firmware?), puedes hacerlo sin problemas con el mismo código que escribirías para cualquier otra comunicación HTTP en tu app.

La ruta de llamada simulada tiene este aspecto:

  1. Llama a un método de uno de los objetos FlutterBleLib (BleManager, Peripheral, Service, Characteristic)
  2. El código de Dart envía el nombre del método y sus parámetros al puente nativo
  3. El puente nativo recibe los datos y los deserializa si es necesario
  4. El puente nativo llama al método apropiado en el SimulatedAdapter

    La parte del cambio comienza ahora

  5. El BleAdapter -en este caso el del simulador- reenvía la llamada al puente nativo del BLEmulator
  6. El BLEmulator hace la lógica por sí mismo (si no implica al periférico) o llama al método apropiado en el periférico suministrado por el usuario
  7. La respuesta se pasa al puente nativo del BLEmulator
  8. El puente nativo del BLEmulator pasa la respuesta al SimulatedAdapter
  9. El SimulatedAdapter responde al puente nativo

    Vuelve al flujo original

  10. El puente nativo mapea la respuesta y la envía a Dart
  11. Dart analiza la respuesta y la devuelve al llamador original

De esta forma utilizas todo tu código de manejo de BLE y trabajas con los tipos proporcionados por FlutterBleLib sin importar el backend que estés utilizando, ya sea la pila BT del sistema real o la simulación. Esto también significa que usted puede probar la interacción con un periférico en las pruebas automatizadas en su CI!

Cómo usarlo

Hemos cubierto cómo funciona y qué posibilidades proporciona, así que ahora vamos a saltar en cómo usarlo.

  1. Agregar la dependencia a blemulator en su pubspec.yml
  2. Crea tu propio periférico simulado utilizando las clases proporcionadas por el plugin SimulatedPeripheral, SimulatedService y SimulatedCharacteristic (lo cubriré en detalle en la siguiente sección)
  3. Añade el periférico al BLEmulator usando Blemulator.addPeripheral(SimulatedPeripheral)
  4. Llama a Blemulator.simulate() antes de llamar a BleManager.createClient() desde FlutterBleLib

¡Eso es todo, sólo cuatro pasos y ya estás funcionando! Bueno, reconozco que el paso más complejo me lo he saltado un poco, así que vamos a hablar del segundo punto: definir el periférico.

Contrato del periférico

Basaré los siguientes ejemplos en el CC2541 SensorTag de Texas Instruments, centrándome en el sensor de temperatura IR.

Necesitamos saber cómo son los UUID del servicio y sus características. Nos interesan dos lugares de la documentación.

UUIDs que nos interesan:

  • Servicio de temperatura IR: F000AA00-0451-4000-B000-000000000000Este servicio contiene todas las características pertenecientes al sensor de temperatura.
  • Datos de temperatura IR: F000AA01-0451-4000-B000-000000000000

    Los datos de temperatura que se pueden leer o supervisar. El formato de los datos es ObjectLSB:ObjectMSB:AmbientLSB:AmbientMSB. Emitirá una notificación cada periodo configurable mientras esté monitorizado.

  • Configuración de la temperatura IR: F000AA02-0451-4000-B000-000000000000

    Interruptor de encendido/apagado del sensor. Hay dos valores válidos para esta característica::

    • 00-sensor puesto a dormir (los datos de temperatura IR serán cuatro bytes de ceros)
    • 01-sensor habilitado (los datos de temperatura IR emitirán lecturas correctas)
  • Periodo de temperatura IR: F000AA03-0451-4000-B000-000000000000

    El intervalo entre notificaciones.El límite inferior es 300 ms, el límite superior es 1000 ms. El valor de la característica se multiplica por 10, por lo que los valores soportados están entre 30 y 100.

¡Eso es todo lo que buscábamos, así que vamos a la implementación!

Periférico más sencillo

La simulación más sencilla aceptará cualquier valor y tendrá éxito en todas las operaciones.

En Dart se ve así:

Corto y conciso, parece casi un JSON.

Esto es lo que sucede: creamos un periférico llamado SensorTag que tiene un ID especificado en tiempo de ejecución (cualquier cadena está bien, pero debe ser única entre los periféricos conocidos por BLEmulator). Mientras el escaneo del periférico esté activado se anunciará utilizando la información de escaneo por defecto cada 800 milisegundos. Contiene un servicio, cuyo UUID está contenido en los datos del anuncio. El servicio contiene 3 características, al igual que en un dispositivo real, y todas ellas son legibles. La primera de las características no se puede escribir en ella, pero admite notificaciones; las otras dos no se pueden supervisar, pero sí escribir en ellas. No hay valores no válidos para las características. El argumento convenienceName no es utilizado de ninguna manera por el BLEmulator, pero facilita la lectura de la definición.

IR Temperature Config y IR Temperature Period aceptan y establecen cualquier valor que se les pase. La característica IR Temperature Data admite notificaciones, pero en realidad nunca envía ninguna, ya que no las hemos definido de ninguna manera.

BLEmulator proporciona el comportamiento básico fuera de la caja, cuidando el camino feliz para todas sus construcciones, minimizando la cantidad de trabajo necesario. Aunque puede ser suficiente para algunas pruebas y comprobaciones básicas de cumplimiento de especificaciones, no intenta comportarse como un dispositivo real.

¡Necesitamos implementar un comportamiento personalizado!

Hemos mantenido tanto la simplicidad como la flexibilidad al crear BLEmulator. Queríamos dar a los desarrolladores el control necesario sobre cada aspecto del periférico creado, a la vez que requerir el menor trabajo posible por su parte para crear una definición funcional. Para lograr este objetivo, hemos decidido crear la implementación por defecto de todos los métodos que podrían ser su punto de entrada para el comportamiento personalizado y dejar que usted decida qué anular.

Ahora, vamos a añadir algo de lógica.

Hacer que la conexión tome tiempo

Un periférico real probablemente tomará algún tiempo antes de conectarse. Para conseguirlo, sólo hay que anular un método:

Eso es todo. Ahora la conexión tarda 200 milisegundos!

Denegando la conexión

Un caso similar. Queremos mantener el retardo, pero devolver un error de que no se ha podido establecer la conexión. Podrías anular el método y lanzar tú mismo un SimulatedBleError, pero también puedes hacer:

 @override Future<bool> onConnectRequest() async { await Future.delayed(Duration(milliseconds: 200)); return false; }

Desconexión inicializada por el periférico

Digamos que quieres comprobar el proceso de reconexión o simular que te sales del rango. Puedes pedirle a un compañero que corra al otro lado de la oficina con un periférico real, o añadir algún botón de depuración y en su onPress hacer:

yourPeripheralInstance.onDisconnect();

(Aunque la primera opción parece más satisfactoria.)

Modificar el RSSI en la información de escaneo

Digamos que queremos clasificar los periféricos por su intensidad de señal percibida y necesitamos probarlo. Creamos unos cuantos periféricos simulados, dejamos uno con RSSI estático y en el otro hacemos:

@overrideScanResult scanResult() { scanInfo.rssi = -20 - Random.nextInt(50); return super.scanResult();}

De esta forma, puedes tener un par de dispositivos con RSSI variable y probar la función.

Negociar MTU

BLEmulator hace la mayor parte de la lógica por sí mismo, limitando así el MTU al rango soportado de 23 a 512, pero si necesitas limitarlo más, debes anular el método requestMtu():

BLEmulator negociará automáticamente el MTU más alto soportado en iOS.

Forzar valores al rango soportado

Para limitar los valores aceptados por una característica, hay que crear una nueva clase que extienda SimulatedCharacteristic.

La característica ahora limita la entrada a 0 o 1 en el primer byte, ignora cualquier byte adicional y devuelve un error si el valor excede el rango soportado. Para soportar la devolución de un error, una característica tiene que devolver una respuesta a la operación de escritura, por lo tanto, establecer writableWithoutResponse a falso.

Encender el sensor

Nos gustaría saber cuando el sensor de temperatura se enciende o se apaga.

Para conseguirlo, crearemos un nuevo servicio con UUIDs codificados:

La característica que hemos definido no puede ser monitorizada a través de FlutterBleLib, ya que le falta isNotifiable: true, pero puede ser, para su comodidad, monitorizada a nivel de BLEmulator. Esto facilita el control del flujo global que estamos simulando, simplifica la estructura y nos permite evitar extensiones innecesarias de las clases base.

Emisión de notificaciones

Todavía nos falta emitir notificaciones de la característica IR Temperature Data. Vamos a ocuparnos de ello.

_emitTemperature() se llama en el constructor del Servicio de Temperatura y se ejecuta en un bucle infinito. Cada intervalo, especificado por el valor de la característica IR Temperature Period, comprueba si hay un oyente (isNotifying). Si hay uno, entonces escribe datos (ceros o un valor aleatorio, dependiendo de si el sensor está encendido o apagado) en la característica Datos de Temperatura IR. SimulatedCharacteristic.write() notifica a cualquier oyente activo el nuevo valor.

Periférico avanzado en acción

Puedes encontrar un ejemplo completo de un periférico más avanzado en el repositorio de BLEmulator. Si quieres probarlo, sólo tienes que clonar el repositorio y ejecutar su ejemplo.

Uso en pruebas automatizadas

Un gran agradecimiento, aquí, a mi compañero Polidean Paweł Byszewski por su investigación sobre el uso de BLEmulator en pruebas automatizadas.

Flutter tiene un contexto de ejecución diferente para la app probada y la propia prueba, lo que significa que no puedes simplemente compartir un periférico simulado entre los dos y modificar el comportamiento desde la prueba. Lo que puedes hacer es añadir un manejador de datos al controlador de la prueba usando enableFlutterDriverExtension(handler: DataHandler), pasar los periféricos simulados al main() de tu app y pasar mensajes de cadena al manejador dentro del contexto de ejecución de la app.

Se reduce a:Wrapper para la app

Tu periférico

Tu test

Gracias a este mecanismo, podrías inicializar el periférico como quisieras y llamar a cualquiera de los comportamientos que tengas predefinidos dentro de tu dispositivo simulado.

Compruébalo tú mismo

¡Lo mejor de todo esto es que no tienes que creerme! Compruébalo tú mismo en GitHub. Úsalo, diviértete con él, intenta romperlo y cuéntanoslo todo

¿También te gustaría verlo en otras plataformas? Ponte en contacto con nosotros y lo haremos realidad. Asegúrese de revisar nuestras otras bibliotecas, y no dude en ponerse en contacto con nosotros si desea que trabajemos en su proyecto.