Un beacon BLE es un dispositivo que emite datos periódicamente sin aceptar una conexión.
Un termómetro de sala puede limitarse a emitir “24 ºC” sin aceptar órdenes. Una baliza de museo puede publicar un identificador que permita estimar la proximidad.
Para esto existen los Beacons (o Broadcasters). Son dispositivos que no aceptan conexiones. Simplemente lanzan sus datos al aire periódicamente y se vuelven a dormir.
Es la forma más eficiente de transmitir pequeños datos (telemetría básica) con el mínimo consumo de energía posible.
Broadcaster vs peripheral
En la terminología GAP que vimos:
- Peripheral: Se anuncia y espera que alguien se conecte.
- Broadcaster: Se anuncia y le da igual si hay alguien escuchando o no.
En Zephyr, la diferencia principal en el código radica en el tipo de anuncio (BT_LE_ADV_NCONN en lugar de BT_LE_ADV_CONN) y en cómo estructuramos los datos.
El payload: manufacturer data
El paquete de anuncio legacy que usaremos tiene un máximo de 31 bytes. Es poco, pero suficiente para enviar un ID, una temperatura y una batería; los anuncios extendidos admiten cargas mayores.
Para enviar datos propios (“custom”), el estándar Bluetooth define un tipo de dato llamado Manufacturer Specific Data
Este campo tiene una estructura:
- Company ID (2 bytes): Un código asignado por el Bluetooth SIG a cada empresa (Apple es 0x004C, Nordic es 0x0059).
- Data (N bytes): Lo que queramos enviar.
Para pruebas y desarrollo, el estándar reserva el ID 0xFFFF. Usaremos este para no violar las normas del protocolo mientras aprendemos.
Código: un beacon de temperatura
Vamos a crear un dispositivo que simula leer una temperatura y la actualiza en el paquete de anuncio cada segundo.
Definir la estructura de datos
Primero, definimos qué queremos enviar. Vamos a enviar un contador y un valor de temperatura flotante (simulado).
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/sys/byteorder.h>
/* Definimos nuestro Company ID de pruebas */
#define TEST_COMPANY_ID 0xFFFF
/* Buffer con formato explícito: Company ID, Device ID y temperatura x100 */
static uint8_t mfg_data[6];
static int16_t temperature = 2500;
static void encode_mfg_data(void)
{
sys_put_le16(TEST_COMPANY_ID, &mfg_data[0]);
sys_put_le16(0x1234, &mfg_data[2]);
sys_put_le16((uint16_t)temperature, &mfg_data[4]);
}Definir el paquete de anuncio
Creamos el array bt_data. Fíjate que usamos el tipo BT_DATA_MANUFACTURER_DATA.
static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_NO_BREDR), /* No conectable */
/* Aquí metemos nuestra estructura "cruda" */
BT_DATA(BT_DATA_MANUFACTURER_DATA, mfg_data, sizeof(mfg_data)),
};Iniciar el broadcaster
En el main, iniciamos el stack y empezamos a anunciar.
Importante: Usamos BT_LE_ADV_NCONN (Non-Connectable). Si intentas conectaros a este dispositivo con el móvil, dará error o ni siquiera aparecerá el botón “Connect”.
int main(void)
{
int err;
err = bt_enable(NULL);
if (err) {
printk("Error bt_enable (err %d)\n", err);
return err;
}
encode_mfg_data();
/* Arrancamos el anuncio NO CONECTABLE */
/* Parametros:
- Opciones: BT_LE_ADV_OPT_NONE
- Intervalo Min: 800 (x 0.625ms = 500ms)
- Intervalo Max: 801
*/
struct bt_le_adv_param *adv_param =
BT_LE_ADV_PARAM(BT_LE_ADV_OPT_NONE, 800, 801, NULL);
err = bt_le_adv_start(adv_param, ad, ARRAY_SIZE(ad), NULL, 0);
if (err) {
printk("Error al anunciar (err %d)\n", err);
return err;
}
printk("Beacon iniciado. Enviando datos...\n");
/* Bucle principal: Actualizar datos dinámicamente */
while (1) {
k_sleep(K_SECONDS(2));
/* Simulamos cambio de temperatura */
temperature += 10; // +0.1 ºC
if (temperature > 3000) {
temperature = 2000;
}
encode_mfg_data();
printk("Actualizando temperatura a: %d\n", temperature);
/* Actualizar el anuncio en caliente */
err = bt_le_adv_update_data(ad, ARRAY_SIZE(ad), NULL, 0);
if (err) {
printk("Fallo al actualizar (err %d)\n", err);
}
}
}Analizando el resultado con nRF Connect
Compila y flashea este código; después abre nRF Connect en tu móvil.
- Id a la pestaña Scanner.
- Busca un dispositivo (probablemente saldrá como “N/A” o con el nombre si lo añadisteis, aunque los beacons puros a veces no envían nombre para ahorrar bytes).
- Desplegad los detalles.
- Busca el campo Manufacturer Data.
Verás algo como: 0xFFFF3412C409.
FFFF: Company ID.3412: Device ID0x1234codificado en little-endian.C409: 2500 codificado en little-endian (25,00 ºC).
¡Verás cómo el valor cambia cada 2 segundos sin necesidad de conectaros!
Eddystone y iBeacon
Quizá te suenen iBeacon (Apple) o Eddystone (Google). Son formatos definidos sobre el mismo mecanismo de anuncios que acabamos de utilizar.
En lugar de inventarse la estructura my_beacon_data_t, Apple y Google definieron: “El byte 1 es el UUID, el byte 2 es la potencia, el byte 3 es la URL…”.
En Zephyr, implementar un iBeacon es simplemente rellenar el array bt_data con los bytes que dicta la especificación de Apple.
Consumo de energía
Un Broadcaster es el rey de la batería.
- La radio solo se enciende durante unos pocos milisegundos cada 500ms (o lo que definamos).
- No tiene que escuchar (RX) esperando peticiones de conexión.
Con intervalos largos y un diseño de bajo consumo, una pila de botón puede durar mucho tiempo. La autonomía real depende del SoC, la potencia de radio, el intervalo, los sensores y la capacidad útil de la batería.