Un servicio GATT es un conjunto de atributos que organiza los datos y operaciones de un dispositivo Bluetooth LE.
Cuando conectas tu reloj inteligente al móvil, no solo ves la hora. El reloj envía tu ritmo cardíaco en tiempo real, y tú puedes enviarle configuraciones o cambiar la esfera desde la App. Eso es una conexión bidireccional basada en GATT (Generic Attribute Profile).
En Zephyr, definir esta estructura es increíblemente elegante gracias a su sistema de macros. Vamos a construir un ejemplo real: un Servicio de Sensor que envía datos de temperatura al móvil y permite controlar un LED desde la App.
La estructura: servicios y características
Podemos pensar en GATT como una base de datos organizada en varios niveles:
Perfil (Profile): Es la colección completa (ej. “Monitor Cardíaco”).
Servicio (Service): Es una carpeta lógica (ej. “Medición de Pulso”, “Información del Dispositivo”).
Característica (Characteristic): Es el archivo con el dato (ej. “Valor BPM”, “Ubicación del sensor”).
Cada característica tiene:
- Valor: El dato en sí.
- Propiedades: ¿Se puede leer (
READ)? ¿Se puede escribir (WRITE)? ¿Avisa cuando cambia (NOTIFY)? - Descriptores: Metadatos (ej. la unidad de medida, o el famoso CCC para activar notificaciones).
Los UUID (identificadores)
Como vamos a crear un servicio propio (no es un estándar como el de Batería), necesitamos generar UUIDs de 128 bits aleatorios para no chocar con nadie.
Puedes usar un generador online (busca “UUID generator”).
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
/* UUID del Servicio: 12345678-1234-5678-1234-56789abcdef0 */
#define MY_SERVICE_UUID_VAL \
BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef0)
/* UUID de la Característica de Temperatura */
#define MY_CHAR_UUID_VAL \
BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef1)
static struct bt_uuid_128 my_service_uuid = BT_UUID_INIT_128(MY_SERVICE_UUID_VAL);
static struct bt_uuid_128 my_char_uuid = BT_UUID_INIT_128(MY_CHAR_UUID_VAL);Definir el servicio con macros
Zephyr permite declarar esta estructura de forma estática mediante la macro BT_GATT_SERVICE_DEFINE.
Vamos a definir una característica que sea:
- Leíble (Read): El móvil puede preguntar el valor.
- Notificable (Notify): El dispositivo empuja el valor al móvil cuando cambia.
/* Variable donde guardamos el dato real */
static uint32_t temperature_value = 0;
/* Callback de lectura: Se llama cuando el móvil pide leer el dato */
static ssize_t read_temperature(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
/* Enviamos el valor actual de la variable */
const char *value = (const char *)&temperature_value;
return bt_gatt_attr_read(conn, attr, buf, len, offset, value, sizeof(temperature_value));
}
/* Callback cuando cambia la configuración de notificaciones (CCC) */
static void my_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
bool notif_enabled = (value == BT_GATT_CCC_NOTIFY);
printk("Notificaciones %s\n", notif_enabled ? "activadas" : "desactivadas");
}
/* DEFINICIÓN DEL SERVICIO */
BT_GATT_SERVICE_DEFINE(my_sensor_svc,
/* 1. Declaramos que es un Servicio Primario */
BT_GATT_PRIMARY_SERVICE(&my_service_uuid.uuid),
/* 2. Definimos la Característica */
/* Parametros: UUID, Propiedades, Permisos, Callback Lectura, Callback Escritura, Valor Inicial */
BT_GATT_CHARACTERISTIC(&my_char_uuid.uuid,
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, /* Propiedades */
BT_GATT_PERM_READ, /* Permisos (Seguridad) */
read_temperature, NULL, NULL),
/* 3. Definimos el Descriptor CCC (Cliente Characteristic Configuration) */
/* ES OBLIGATORIO si usamos NOTIFY o INDICATE */
BT_GATT_CCC(my_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
);El CCC es imprescindible para las notificaciones. Si pones BT_GATT_CHRC_NOTIFY pero olvidas la macro BT_GATT_CCC, el cliente no podrá suscribirse a los cambios.
El código principal (main)
Ahora necesitamos:
- Iniciar Bluetooth.
- Empezar a anunciar (como Conectable).
- Simular cambios de temperatura y Notificar.
/* Datos de anuncio (ahora incluimos el UUID del servicio para que el móvil sepa qué hacemos) */
static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA_BYTES(BT_DATA_UUID128_ALL, MY_SERVICE_UUID_VAL),
};
int main(void)
{
int err;
err = bt_enable(NULL);
if (err) {
printk("Error init (err %d)\n", err);
return err;
}
/* Anunciamos como CONECTABLE (BT_LE_ADV_CONN) */
err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0);
if (err) {
printk("Error advertising (err %d)\n", err);
return err;
}
printk("Listo. Conéctate con nRF Connect.\n");
while (1) {
k_sleep(K_SECONDS(1));
/* Simulamos lectura de sensor */
temperature_value++;
/* NOTIFICACIÓN (PUSH) */
/* Buscamos el atributo dentro del servicio y enviamos el cambio */
/* attrs[1] es la declaración y attrs[2] contiene el valor. */
/* Usamos bt_gatt_notify(conn, attr, data, len) */
/* Si conn es NULL, notifica a TODOS los conectados */
bt_gatt_notify(NULL, &my_sensor_svc.attrs[2], &temperature_value,
sizeof(temperature_value));
}
}Probar con el móvil
- Abre nRF Connect.
- Escanead y busca tu dispositivo (verás que anuncia un UUID largo).
- Pulsa CONNECT.
- Se abrirá una nueva pestaña. Verás “Generic Access”, “Generic Attribute” y tu Unknown Service (con tu UUID).
- Pulsa en el servicio. Verás la característica.
- Pulsa el icono de Download (flecha abajo): leerás el valor actual.
- Pulsa el icono de suscripción: el valor empezará a actualizarse cada segundo sin nuevas lecturas manuales.
Recibir datos (write)
¿Y si queremos encender un LED desde el móvil? Necesitamos una característica con propiedad WRITE.
/* Callback de escritura */
static ssize_t write_led(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset,
uint8_t flags)
{
const uint8_t *val = buf;
if (offset != 0) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
if (len != 1) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
}
if (*val == 1) {
printk("LED ENCENDIDO desde el móvil\n");
// gpio_pin_set(...)
} else {
printk("LED APAGADO desde el móvil\n");
}
return len;
}
/* En la definición del servicio añadimos: */
BT_GATT_CHARACTERISTIC(&my_led_uuid.uuid,
BT_GATT_CHRC_WRITE,
BT_GATT_PERM_WRITE,
NULL, write_led, NULL),En nRF Connect, verás un icono de “Flecha arriba”. Al pulsarlo, podrás enviar un byte (01 o 00) y verás el mensaje en la consola de Zephyr.