Files
org-hub/learn/BLE_ESP32_Fundamentals.md

52 KiB

BLE + ESP32: Masterclass Teórico-Práctico

De Cero a Experto en Bluetooth Low Energy y Desarrollo Embedded


TABLA DE CONTENIDOS

  1. Módulo 0: Fundamentos de BLE
  2. Módulo 1: Advertising & Discovery
  3. Módulo 2: Connections & GATT
  4. Módulo 3: Security & Advanced Topics
  5. Debugging Masterclass
  6. Ejercicios Prácticos Progresivos
  7. Referencias & Recursos

MÓDULO 0: FUNDAMENTOS DE BLE

0.1 ¿Qué es Bluetooth Low Energy (BLE)?

BLE es una tecnología inalámbrica diseñada para baja potencia, corto alcance y bajo consumo de energía. Se diferencia del Classic Bluetooth (BR/EDR) en:

Característica Classic BT BLE
Potencia ~100 mW ~10 mW promedio
Alcance ~100 m ~100 m (BLE 5.x)
Latencia 100 ms <10 ms
Ancho de banda 720 kbps 1-2 Mbps
Caso de uso Audio, streaming Sensores, IoT, wearables
Stack Complejo (Bluedroid+BR/EDR) Simplificado (NimBLE BLE-only)

¿Por qué BLE importa en IoT/Edge AI?

  • Actualizaciones de modelos ML en dispositivos wearables sin descargar batería.
  • Comunicación entre edge devices (sensores + nodos) con bajo overhead.
  • Integración con blockchain: coordinación de múltiples IoT nodes.

0.2 Arquitectura del Stack BLE

El stack BLE se organiza en capas, de forma similar a OSI:

┌─────────────────────────────────────┐
│  APLICACIÓN (Application)           │  ← Tu código
├─────────────────────────────────────┤
│  GATT (Generic Attribute Profile)   │  ← Servicios y características
├─────────────────────────────────────┤
│  GAP (Generic Access Profile)       │  ← Advertising, conexiones
├─────────────────────────────────────┤
│  ATT (Attribute Protocol)           │  ← Lectura/escritura de atributos
├─────────────────────────────────────┤
│  L2CAP (Logical Link Control)       │  ← Multiplexación de canales
├─────────────────────────────────────┤
│  Link Layer (Enlace de datos)       │  ← Control de conexión, hop freq
├─────────────────────────────────────┤
│  PHY (Física)                       │  ← Radio 2.4 GHz, modulación
└─────────────────────────────────────┘
   │
   ├─── CONTROLLER ───────────────────── (Firmware en el chip)
   │      (HCI: Host Controller Interface)
   │
   └─── HOST ─────────────────────────── (Software en tu aplicación)
        (Bluedroid o NimBLE en ESP32)

Conceptos Clave:

1. Controller vs Host

  • Controller: Firmware bajo nivel en el chip (RF, Link Layer). No lo tocas en ESP32; está pre-compilado.
  • Host: Software que interpreta comandos del aplicación y genera HCI commands para el controller.
  • HCI (Host Controller Interface): Protocolo de comunicación entre host y controller. En ESP32 usa transporte RAM (no UART).

2. Roles BLE

  • Peripheral (Esclavo): Anuncia datos, acepta conexiones. Ej: reloj inteligente.
  • Central (Maestro): Escanea, inicia conexiones. Ej: smartphone.

3. Estados GAP

  • Standby: Inactivo.
  • Advertising: Transmite paquetes de publicidad.
  • Scanning: Escucha paquetes de publicidad.
  • Connected: Vinculado con otra dispositivo.

0.3 Physical Layer (PHY): La Radio

Banda de Frecuencia

BLE opera en la banda ISM (Industrial, Scientific, Medical) de 2.4 GHz, compartida con WiFi, Zigbee, etc.

┌─────────────────────────────────────────────────────────────┐
│ 2.4 GHz ISM Band (2400 - 2483.5 MHz)                        │
├─────────────────────────────────────────────────────────────┤
│ BLE:  37 canales de datos + 3 canales de advertising         │
│       Espaciados cada 2 MHz                                  │
│                                                              │
│ Ch 37: 2402 MHz (Adv)                                        │
│ Ch 38: 2426 MHz (Adv)                                        │
│ Ch 39: 2480 MHz (Adv)                                        │
│                                                              │
│ Ch 0-36: 2404 - 2480 MHz (Datos + algunas adv ext)          │
└─────────────────────────────────────────────────────────────┘

Modulación y Velocidades (PHYs)

PHY Velocidad Modulación Uso
LE 1M (default) 1 Mbps GFSK Compatibilidad, bajo costo
LE 2M (BLE 5.0+) 2 Mbps GFSK Mayor throughput, menos batería en tx
LE Coded (BLE 5.0+) 125/500 kbps GFSK + FEC Largo alcance, menor SNR

Nota: Todos los chips ESP32 modernos soportan LE 1M. LE 2M y LE Coded dependen de la variante (C3, S3, etc.).

Formato de Paquete BLE (Uncoded PHY)

┌───────┬──────────────┬───────────┬─────┐
│Preamble│Access Address│   PDU     │ CRC │
│ 1 byte │  4 bytes    │  2-257B   │3 byte│
└───────┴──────────────┴───────────┴─────┘
         └────────────────────────────────┘
              Link Layer Packet (LL PDU)
  • Preamble (0xAA/0x55): Sincronización de frecuencia.
  • Access Address (4B): ID del dispositivo o conexión.
  • PDU: Datos; puede ser advertising PDU o data channel PDU.
  • CRC (3B): Cyclic Redundancy Check para detectar errores.

Frequency Hopping (AFH)

BLE evita colisiones e interferencias usando Adaptive Frequency Hopping (AFH):

  1. Secuencia pseudo-aleatoria: Los dispositivos emparejados comparten un Access Address y un hop increment (derivado de direcciones MAC).
  2. Salto por conexión: Salta entre los 37 canales de datos cada connection interval (7.5 ms - 4 s).
  3. Channel map: Si ciertos canales tienen mucho ruido, se excluyen dinámicamente.

Fórmula: Nuevo canal = (Anterior + hop_increment) % 37

# Pseudocódigo del hop
def next_channel(current_ch, hop_inc, channel_map):
    """Calcula el próximo canal en el hop."""
    unmapped_channels = channel_map  # Máscara de 37 bits
    
    next_unmapped = (current_ch + hop_inc) % 37
    # Si está excluido, reintenta con el siguiente
    while not test_bit(unmapped_channels, next_unmapped):
        next_unmapped = (next_unmapped + 1) % 37
    
    return next_unmapped

Paquetes Advertising PDU

Formato de un paquete de publicidad:

┌──────────────────┬─────────────────────────┐
│ Advertising Hdr  │    Advertising Payload  │
├──────────────────┼─────────────────────────┤
│ Tipo | Tx/Rx Adr │   MAC + Datos Payload   │
│ 1B   │   1B      │      ≤ 255 bytes        │
└──────────────────┴─────────────────────────┘

Tipos de Advertising PDU:

  • ADV_IND (0x00): Connectable undirected (periférico abierto).
  • ADV_DIRECT_IND: Connectable directed (a un central específico).
  • ADV_NONCONN_IND: Non-connectable (beacon).
  • ADV_SCAN_IND: Scannable undirected (periférico + scan response).
  • SCAN_RSP: Respuesta a scan request.

0.5 ATT & GATT: Estructura de Datos

ATT (Attribute Protocol)

ATT es el protocolo crudo de lectura/escritura de atributos (bloques de datos con UUID).

Operaciones ATT:

Cliente (Central)              Servidor (Peripheral)
    │                                   │
    ├─ Read Request ────────────────>  │
    │                    Read Response  │
    │  <────────────────────────────────┤
    │                                   │
    ├─ Write Request ───────────────>  │
    │                   Write Response  │
    │  <────────────────────────────────┤
    │                                   │
    ├─ Notify (unsolicited) <─────────  │
    │  (sin confirmación)               │
    │                                   │
    ├─ Indicate (unsolicited) <──────  │
    │  (con confirmación)               │
    │  ─── Confirmation ───────────>   │

GATT (Generic Attribute Profile)

GATT es una capa de semántica sobre ATT que organiza atributos en:

  1. Servicios: Colecciones de funcionalidad. Ej: Heart Rate Service (HRS).
  2. Características: Propiedades dentro de un servicio. Ej: Heart Rate Measurement.
  3. Descriptores: Metadatos de una característica. Ej: CCCD (Client Characteristic Configuration).

Estructura GATT:

Service (UUID 0x180D = Heart Rate Service)
  ├─ Characteristic 1 (UUID 0x2A37 = Heart Rate Measurement)
  │  ├─ Value: [BPM value]
  │  ├─ Properties: Read, Notify
  │  └─ Descriptor (CCCD): [0x01, 0x00] = Notificaciones enabled
  │
  └─ Characteristic 2 (UUID 0x2A38 = Body Sensor Location)
     ├─ Value: [0x01 = Chest]
     └─ Properties: Read

CCCD (Client Characteristic Configuration Descriptor):

  • Bitmask que el cliente escribe para activar/desactivar notificaciones.
  • 0x0001: Notificaciones.
  • 0x0002: Indicaciones.

0.6 Bluedroid vs NimBLE: Comparativa Técnica

ESP-Bluedroid

Arquitectura:

┌─────────────────┐
│ esp_* APIs      │  ← Tu código
├─────────────────┤
│ BTC (Bluetooth  │  ← Profile manager, HCI handlers
│ Transport Ctrl) │
├─────────────────┤
│ BTU (Bluetooth  │  ← L2CAP, GATT, SMP, ATT
│ Upper Layer)    │
├─────────────────┤
│ HCI Interface   │  ← Comandos al controller
└─────────────────┘

Características:

  • Soporta Classic BT (BR/EDR) + BLE.
  • Memora RAM: ~200 KB heap típicamente.
  • Velocidad init: ~631 ms (ver benchmark).
  • Mayor footprint de código.

Cuándo usar Bluedroid:

  • Necesitas Classic BT (audio Bluetooth clásico).
  • Compatibilidad máxima con código Android.

Apache NimBLE

Arquitectura:

┌─────────────────┐
│ ble_* APIs      │  ← Tu código
├─────────────────┤
│ NimBLE Host     │  ← Task separado en FreeRTOS
│ (event-driven)  │
├─────────────────┤
│ HCI Interface   │  ← Comandos al controller
└─────────────────┘

Características:

  • BLE-only (no Classic BT).
  • RAM: ~70-80 KB heap, mucho más eficiente.
  • Velocidad init: ~209 ms (3x más rápido).
  • Open-source Apache license.
  • Mejor para deep sleep / battery-powered devices.

Cuándo usar NimBLE:

  • Solo necesitas BLE (99% de IoT moderno).
  • Quieres optimizar potencia y memoria.
  • Prefieres open-source.

Recomendación

🎯 Para tu roadmap (Edge AI + IoT + Blockchain):

Usa NimBLE porque:

  1. Menor footprint → más espacio para ML models.
  2. Mejor para federated learning (múltiples nodos IoT).
  3. Comunidad open-source muy activa (Apache).
  4. Por defecto moderno en nuevos proyectos ESP32.

0.7 Seguridad BLE: Introducción Conceptual

Legacy Pairing (BLE 4.1 y anteriores)

  1. Intercambio de claves Temporary Link Key (TK) basado en IO capabilities.
  2. Generación STK (Short Term Key) via algoritmo simple (vulnerable).
  3. Encriptación de la conexión con STK.

Vulnerabilidades conocidas:

  • KNOB attack (Key Negotiation Of Bluetooth).
  • Downgrade attacks.

LE Secure Connections (LESC, BLE 4.2+)

  1. Generación de pares ECDH (Elliptic Curve Diffie-Hellman) en cada dispositivo.
  2. Intercambio de claves públicas (seguro matemáticamente).
  3. Generación LTK (Long Term Key) determinista y fuerte.
  4. Variantes de autenticación:
    • Numeric Comparison: Ambos muestran 6 dígitos; usuario confirma si coinciden.
    • Passkey Entry: Uno muestra código, otro lo digita.
    • Just Works: Sin confirmación (sin MITM protection).
    • Out Of Band (OOB): Intercambio de claves por canal alternativo (NFC, etc.).

Matemática simplificada:

Dispositivo A: d_a = random(); Q_a = d_a * G (ECDH público)
Dispositivo B: d_b = random(); Q_b = d_b * G

Intercambio: Q_a <-> Q_b

Shared secret: DHKey = d_a * Q_b = d_b * Q_a = (d_a * d_b) * G

LTK = PRES(DHKey, other_params)  [derivado del shared secret]

En ESP32:

  • LESC es el default en NimBLE.
  • Generación ECDH es blocking ~160 ms (limitación ROM).
  • Requiere supervision_timeout >= 165 ms durante pairing.

0.8 Consumo de Potencia y Parámetros Críticos

Ecuación General

Consumo promedio = (T_on * I_active + T_sleep * I_sleep) / T_total

Parámetros de Conexión

Parámetro Rango Impacto
Connection Interval 7.5 ms - 4 s Menor = mayor latencia pero más potencia
Peripheral Latency 0 - 499 Nodo puede saltear N intervalos (reduce despertares)
Supervision Timeout 100 ms - 32 s Si no recibe dato en este tiempo, desconecta
MTU (Max Transfer Unit) 23 - 251 bytes Mayor MTU = menos fragmentación, menos paquetes

Ejemplo optimizado para batería:

// Connection interval 1 segundo, permite saltar 199 de cada 200 eventos
ble_gap_upd_params conn_params = {
    .itvl_min = 800,       // 1000 ms
    .itvl_max = 800,
    .latency = 199,        // Puede saltear hasta 199 intervalos
    .supervision_timeout = 320,  // 3.2 segundos
    .min_ce_len = 0x00,
    .max_ce_len = 0x00
};

MÓDULO 1: ADVERTISING & DISCOVERY

1.1 Ciclo de Vida del Advertising

Estados del Periférico

    ┌─────────────┐
    │   STANDBY   │
    │  (inactivo) │
    └──────┬──────┘
           │ esp_ble_gap_start_advertising()
           ▼
    ┌─────────────┐
    │ ADVERTISING │◄──────────────────────┐
    │  (cada ~100 │                       │
    │   eventos,  │                       │
    │  hopping    │                       │
    │  canales)   │                       │
    └──────┬──────┘                       │
           │ (remoto) Connection request  │
           ▼                              │
    ┌─────────────┐                       │
    │  CONNECTED  │────► Disconnect ──────┘
    │             │
    └─────────────┘

Advertising Event

Cada N milisegundos:

  1. ESP32 transmite paquete de publicidad en Ch 37.
  2. Espera ~120 μs por SCAN_REQ (si scannable).
  3. Si recibe SCAN_REQ, transmite SCAN_RSP en Ch 37.
  4. Salta a Ch 38, repite.
  5. Salta a Ch 39, repite.
  6. Espera hasta el próximo advertising interval (ej: 100 ms).

1.2 Estructura de Advertising Data

Los datos de publicidad se empaquetan en TLV (Type-Length-Value) blocks:

┌──────┬────────┬─────────────────┐
│ Type │ Length │ Value           │
│ 1B   │ 1B     │ Length bytes     │
└──────┴────────┴─────────────────┘
  ^      ^          ^
  │      │          └─ Contenido real (flags, UUID, etc.)
  │      └─ Tamaño en bytes
  └─ Tipo de dato (0x01 = Flags, 0x08 = Name, etc.)

Tipos Comunes (AD Types)

AD Type Valor Significado
0x01 Flags Propiedades del dispositivo
0x08 Shortened Local Name Nombre corto
0x09 Complete Local Name Nombre completo
0x0A TX Power Level Potencia de transmisión
0x16 Service Data (16-bit UUID) Datos asociados a servicio
0x20 Service Data (32-bit UUID) Idem pero UUID 32-bit
0x21 Service Data (128-bit UUID) Idem pero UUID 128-bit

Flags Comunes

#define ESP_BLE_ADV_FLAG_LIMIT_DISC  0x01   // Limited Discoverable Mode
#define ESP_BLE_ADV_FLAG_GEN_DISC    0x02   // General Discoverable Mode
#define ESP_BLE_ADV_FLAG_BREDR_NOT_SPT 0x04 // BR/EDR Not Supported
#define ESP_BLE_ADV_FLAG_DMR_CONTROLLER 0x08 // Dual Mode Controller
#define ESP_BLE_ADV_FLAG_DMR_HOST    0x10   // Dual Mode Host

Ejemplo de payload de advertising completo:

01 01 06           → Flags: 0x06 (General Discoverable, BR/EDR Not Supported)
09 09 45 53 50 33 32 5F 42 4C 45  → Name: "ESP32_BLE" (9 bytes)
02 0A F4           → TX Power: -12 dBm (0xF4 = -12 en two's complement)

Total: 15 bytes de 31 disponibles

1.3 Beacons: Tipos y Formatos

Los beacons son dispositivos que no necesitan conexión; solo transmiten datos periódicamente.

iBeacon (Apple)

Formato:

Company ID (Apple 0x004C) + Type + Major + Minor + TX Power
    2B              +   1B   +  2B   +  2B  +  1B

Ejemplo en Service Data:

uint8_t adv_data[] = {
    0x02, 0x01, 0x06,            // Flags
    0x1A, 0xFF,                  // Service Data type
    0x4C, 0x00,                  // Company ID: 0x004C (Apple)
    0x02, 0x15,                  // iBeacon type (0x02) + version (0x15)
    // UUID: 12345678-1234-5678-1234-56789ABCDEF0
    0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78,
    0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
    0x12, 0x34,                  // Major: 0x1234
    0x56, 0x78,                  // Minor: 0x5678
    0xC3                         // TX Power: -61 dBm
};

Eddystone (Google/open)

Más flexible que iBeacon. Formatos:

  • Eddystone-URL: URL corta (prefijo comprimido).
  • Eddystone-UID: UUID 16B + instancia (sensores).
  • Eddystone-TLM: Telemetría (batería, temp).
  • Eddystone-EID: Ephemeral ID (privacidad).

1.4 API: Bluedroid Beacon

Código Ejemplo (Conceptual)

#include "esp_gatt_defs.h"
#include "esp_gap_ble_api.h"
#include "esp_bt.h"

// Advertising data: Name + TX Power
static uint8_t adv_data[] = {
    0x02, 0x01, 0x06,                              // Flags
    0x09, 0x09, 'B', 'L', 'U', 'E', 'D', 'R', 'O', 'I', 'D',  // Name
    0x02, 0x0A, 0xF4                               // TX Power: -12 dBm
};
static uint8_t scan_rsp_data[] = {
    0x02, 0x0A, 0xF4,                              // TX Power
    0x0B, 0x09, 'h', 't', 't', 'p', 's', ':', '/', '/', 'e', 's', 'p'
};

// GAP Callback
static void esp_gap_cb(esp_gap_ble_cb_event_t event,
                       esp_ble_gap_cb_param_t *param) {
    switch (event) {
    case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
        ESP_LOGI("GAP", "Advertising data set");
        esp_ble_gap_start_advertising(&adv_params);
        break;
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
        if (param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS) {
            ESP_LOGI("GAP", "Advertising started");
        }
        break;
    // ... más eventos
    }
}

void app_main(void) {
    // Inicializar NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
        nvs_flash_erase();
        nvs_flash_init();
    }

    // Liberar memoria de Classic BT
    esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);

    // Init Bluetooth
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    esp_bt_controller_init(&bt_cfg);
    esp_bt_controller_enable(ESP_BT_MODE_BLE);

    // Init Bluedroid
    esp_bluedroid_init();
    esp_bluedroid_enable();

    // Registrar callback GAP
    esp_ble_gap_register_callback(esp_gap_cb);

    // Configurar parámetros de advertising
    esp_ble_adv_params_t adv_params = {
        .adv_int_min = 0x0020,      // 20 ms
        .adv_int_max = 0x0040,      // 40 ms
        .adv_type = ADV_TYPE_NONCONN_IND,  // Non-connectable
        .own_addr_type = BLE_ADDR_TYPE_PUBLIC,
        .channel_map = ADV_CHNL_ALL,
        .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
    };

    // Establecer datos de advertising
    esp_ble_gap_config_adv_data_raw(adv_data, sizeof(adv_data));
    esp_ble_gap_config_scan_rsp_data_raw(scan_rsp_data, sizeof(scan_rsp_data));
}

1.5 API: NimBLE Beacon

Código Ejemplo (Conceptual)

#include "host/ble_hs.h"
#include "host/util/util.h"
#include "nimble/nimble_port.h"

static uint8_t own_addr_type;

// Inicializar GAP
static int adv_init(void) {
    struct ble_svc_gap_params gap_params;
    struct ble_hs_cfg cfg;
    struct ble_gap_adv_params adv_params;
    struct ble_gap_adv_set_fields adv_fields;

    // Asegurar dirección válida
    ble_hs_util_ensure_addr(0);
    ble_hs_id_infer_auto(0, &own_addr_type);

    // Formato de datos de advertising
    memset(&adv_fields, 0, sizeof(adv_fields));

    // Flags
    adv_fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;

    // Nombre
    adv_fields.name = (uint8_t *)"NimBLE_Beacon";
    adv_fields.name_len = strlen("NimBLE_Beacon");
    adv_fields.name_is_complete = 1;

    // TX Power
    adv_fields.tx_pwr_lvl_is_present = 1;
    adv_fields.tx_pwr_lvl = -12;  // dBm

    rc = ble_gap_adv_set_fields(&adv_fields);
    if (rc != 0) {
        ESP_LOGE("ADV", "ble_gap_adv_set_fields failed: %d", rc);
        return rc;
    }

    // Configurar parámetros de advertising
    memset(&adv_params, 0, sizeof(adv_params));
    adv_params.conn_mode = BLE_GAP_CONN_MODE_NON;  // Non-connectable
    adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;   // General discoverable
    adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(20);  // 20 ms
    adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(40);  // 40 ms

    rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params,
                           NULL, NULL);
    if (rc != 0) {
        ESP_LOGE("ADV", "ble_gap_adv_start failed: %d", rc);
        return rc;
    }

    return 0;
}

// Sync callback (cuando NimBLE está listo)
static void ble_app_on_sync(void) {
    int rc = adv_init();
    assert(rc == 0);
}

void app_main(void) {
    // Inicializar NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
        nvs_flash_erase();
        nvs_flash_init();
    }

    // Init Bluetooth
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    esp_bt_controller_init(&bt_cfg);
    esp_bt_controller_enable(ESP_BT_MODE_BLE);

    // Init NimBLE
    nimble_port_init();

    // Registrar callback sync
    ble_hs_cfg.sync_cb = ble_app_on_sync;

    // Iniciar stack NimBLE
    nimble_port_freertos_init(BLE_HS_TASK_PRIORITY);
}

1.6 Ejercicio Práctico 1: Beacon Custom

Objetivo: Crear un beacon que transmita datos custom (temperatura, estado sensor).

Requisitos:

  1. Usa NimBLE.
  2. Implementa Service Data con UUID 16-bit custom.
  3. Transmite 2 bytes (temperatura + humedad mocked).
  4. Advertising interval: 100 ms.
  5. TX Power: -10 dBm.

Pasos:

  1. Crea un proyecto nuevo en examples/bluetooth/ble_beacon_custom/.
  2. Incluye lectura ADC simulada (temperatura mocked).
  3. Actualiza datos cada 500 ms (sin reiniciar advertising).
  4. Verifica en nRF Connect que los datos cambian.

MÓDULO 2: CONNECTIONS & GATT

2.1 Ciclo de Vida de Conexión

Central (Smartphone, Tuya App, etc.)

SCAN ──> DISCOVER ──> CONNECT ──> GATT ──> DISCONNECT
  │         │           │         │
  │ Busca   │ Envía     │ Espera  │ Lee/
  │ beacons │ LE_CONN   │ CONNECT │ Escribe/
  │ que se  │ _REQ en   │ _IND    │ Notif
  │ anuncian│ Ch 37/38/ │         │
  │         │ 39        │         │

Periférico (ESP32)

ADVERTISING ──> CONNECT_IND ──> CONNECTED ──> [GATT] ──> DISCONNECT
    │               │                 │
    │ Transmite     │ Central envia   │ Intercambia
    │ en 37/38/39   │ LE_CONN_REQ en  │ atributos y
    │               │ cualquier canal │ datos
    │               │ (parámetros)    │

2.2 Connection Request: Parámetros Clave

Cuando un central inicia conexión, negocia:

struct ble_gap_conn_params {
    uint16_t scan_itvl;              // Scan interval (central)
    uint16_t scan_window;            // Scan window (central)
    uint16_t itvl_min;               // Min connection interval (periférico)
    uint16_t itvl_max;               // Max connection interval
    uint16_t latency;                // Peripheral latency
    uint16_t supervision_timeout;    // Supervision timeout
    uint16_t min_ce_len;             // Min connection event length
    uint16_t max_ce_len;             // Max connection event length
};

Connection Interval: Tiempo entre dos eventos de conexión.

  • Rango: 7.5 ms - 4 s (en múltiplos de 1.25 ms).
  • Valores típicos:
    • Low latency: 7.5 - 30 ms (gaming, streaming).
    • Normal: 30 - 100 ms (sensores, actuadores).
    • Battery: 1 - 4 s (wearables, medidores).

Peripheral Latency: Nodo puede ignorar N connection events.

  • Ej: latency=199 en 1s interval = nodo responde cada 200s (5 minutos).
  • Trade-off: Menor consumo vs. menor responsividad.

Supervision Timeout: Máximo tiempo sin recibir datos antes de desconexión.

  • Debe ser > (1 + latency) * interval_max * 2.
  • Ej: interval=100ms, latency=199 → timeout >= 40 segundos.

2.3 MTU Negotiation

MTU (Maximum Transmission Unit): Máximo tamaño de datos útil en ATT.

┌──────────────┬────────────┬─────────────────┐
│ Link Layer   │ L2CAP HDR  │ ATT Data        │
│ (4 bytes)    │ (4 bytes)  │ (= MTU - 3)     │
└──────────────┴────────────┴─────────────────┘
                    │
            Total ≤ 251 bytes (BLE packet limit)
  • Default MTU: 23 bytes → 20 bytes de datos útil.
  • Max MTU: 251 bytes → 248 bytes de datos útil.

Negociación:

Client → Server: MTU Exchange Request (MTU_desired = 128)
Server → Client: MTU Exchange Response (MTU_server = 251)
Negociado: MTU = min(128, 251) = 128

En NimBLE:

// Configurar MTU esperado durante init
ble_hs_cfg.att_max_mtu = 251;

// En runtime, solicitar MTU desde central
ble_gattc_exchange_mtu(conn_handle);

2.4 GATT Services y Characteristics

Estructura Jerárquica

┌────────────────────────────┐
│  SERVICE (HRS 0x180D)      │
│  ├─ Start Handle: 1        │
│  └─ End Handle: 10         │
├────────────────────────────┤
│  CHARACTERISTIC 1 (0x2A37) │
│  ├─ Handle: 2              │
│  ├─ Value Handle: 3        │
│  ├─ Properties: R,N        │
│  │                         │
│  └─ DESCRIPTOR CCCD        │
│     ├─ Handle: 4           │
│     └─ Value: [0x00, 0x00] │
├────────────────────────────┤
│  CHARACTERISTIC 2 (0x2A38) │
│  ├─ Handle: 5              │
│  ├─ Value Handle: 6        │
│  └─ Properties: R          │
└────────────────────────────┘

Declaración de Servicio (Bluedroid)

// Tabla de atributos
static const esp_gatts_attr_db_t gatt_db[GATTS_CHAR_IDX_NB] = {
    // Declaración de servicio
    [0] = {
        {ESP_GATT_AUTO_RSP},
        {
            ESP_UUID_LEN_16,
            (uint8_t *)&primary_service_uuid,  // 0x2800
            ESP_GATT_PERM_READ,
            2,  // 16-bit UUID de servicio
            (uint8_t *)&service_uuid  // 0x180D (HRS)
        }
    },
    // Declaración de característica
    [1] = {
        {ESP_GATT_AUTO_RSP},
        {
            ESP_UUID_LEN_16,
            (uint8_t *)&character_declaration_uuid,  // 0x2803
            ESP_GATT_PERM_READ,
            1,  // Properties
            (uint8_t *)&char_prop_read_notify  // 0x12
        }
    },
    // Valor de característica
    [2] = {
        {ESP_GATT_AUTO_RSP},
        {
            ESP_UUID_LEN_16,
            (uint8_t *)&heart_rate_char_uuid,  // 0x2A37
            ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
            8,
            (uint8_t *)&heart_rate_value  // [BPM]
        }
    },
    // CCCD Descriptor
    [3] = {
        {ESP_GATT_AUTO_RSP},
        {
            ESP_UUID_LEN_16,
            (uint8_t *)&character_client_config_uuid,  // 0x2902
            ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
            2,
            (uint8_t *)&cccd_value  // [0x00, 0x00]
        }
    }
};

Declaración de Servicio (NimBLE)

static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
    {
        .type = BLE_GATT_SVC_TYPE_PRIMARY,
        .uuid = BLE_UUID16_DECLARE(0x180D),  // Heart Rate Service
        .characteristics = (struct ble_gatt_chr_def[]) {
            {
                .uuid = BLE_UUID16_DECLARE(0x2A37),  // HRM
                .access_cb = gatt_svr_chr_access,
                .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
                .descriptors = (struct ble_gatt_dsc_def[]) {
                    {
                        .uuid = BLE_UUID16_DECLARE(BLE_GATT_DSC_CLT_CFG_UUID16),
                        .access_cb = gatt_svr_dsc_access,
                        .flags = BLE_GATT_DSC_F_WRITE,
                    },
                    {0}
                },
            },
            {0}
        },
    },
    {0}
};

2.5 Notificaciones vs Indicaciones

Propiedad Notificación Indicación
Confirmación No
Velocidad Rápida Más lenta
Fiabilidad Sin garantía Garantizada
Caso de uso Streaming datos Eventos críticos
Evento NimBLE BLE_GAP_EVENT_NOTIFY_TX BLE_GAP_EVENT_CONN_UPDATE

Flujo de Indicación:

Peripheral → INDICATE → Central
                           │
                           ▼ Procesa dato
                           │
                           ▼
Peripheral ← CONFIRM ← Central
     ↓
Evento: BLE_GAP_EVENT_INDICATE_TX

Flujo de Notificación:

Peripheral → NOTIFY → Central
                        (sin confirmación)

2.6 Ejercicio Práctico 2: GATT Server + LED Control

Objetivo: Crear un periférico que expone dos características:

  1. LED Control (write): Escribe 0x00 = OFF, 0x01 = ON.
  2. Button Status (read + notify): Lee estado de botón cada 1 segundo.

Requisitos:

  1. Usa NimBLE.
  2. Servicios:
    • Automation IO service (custom UUID 128-bit).
    • LED write characteristic.
    • Button read characteristic (con CCCD).
  3. Connection parameters: interval 30 ms, latency 0, timeout 3 s.
  4. MTU: 128 bytes.

Pasos:

  1. Configura GPIO (LED en Pin 2, Button en Pin 0).
  2. Implementa gatt_svr_chr_access para manejar reads/writes.
  3. Crea task que envíe notificaciones cada 1 segundo.
  4. Verifica en nRF Connect.

MÓDULO 3: SECURITY & ADVANCED TOPICS

3.1 Flujo de Pairing (LESC)

Fase 1: Exchange Public Keys

Central (Smartphone)        Peripheral (ESP32)
        │                            │
        ├─ Pairing Request ────────> │
        │  (IO cap, auth req, etc.)  │
        │                            │
        │ <──── Pairing Response ────┤
        │   (IO cap, bonding flag)   │
        │                            │
        ├─ Public Key (ECDH) ──────> │  [~160 ms blocking]
        │                            │
        │ <── Public Key (ECDH) ─────┤

Fase 2: Authentication

Depende de IO capabilities:

Numeric Comparison:
    Ambos generan random: 0-999999
    Ambos muestran 6 dígitos
    User confirma: "¿Coinciden?" → Yes/No

Passkey Entry:
    Central muestra: 123456
    Periférico: Entrada en UART → 123456
    
Just Works:
    Sin confirmación (sin MITM)

Fase 3: Key Generation

Shared Secret (DHKey) = d_central * Public_Key_periférico
                      = d_periférico * Public_Key_central

LTK (Long Term Key):
    = PRES(DHKey, 256-bit salt, identifiers)
    [Derivación de 128 bits]

CSRK (Connection Signature Resolving Key):
    = Generado para firmar datos [opcionalidad]

IRK (Identity Resolving Key):
    = Permite resolver direcciones privadas

3.2 NVS: Almacenamiento de Bonding

NVS (Non-Volatile Storage): Partición en flash donde persisten pares.

Estructura en ESP32:

Flash
├─ Partition: nvs
│  ├─ Namespace: peer_0
│  │  ├─ ltk (16 bytes)
│  │  ├─ ediv (2 bytes)
│  │  ├─ rand (8 bytes)
│  │  ├─ csrk (16 bytes)
│  │  ├─ irk (16 bytes)
│  │  └─ identity_address (6 bytes)
│  │
│  ├─ Namespace: peer_1
│  └─ ...

API NimBLE:

// Almacenar automáticamente al parear
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;

// Restaurar bonded devices al reconnectar
static void ble_app_on_sync(void) {
    // NimBLE automáticamente busca pares en NVS
    // Si encontrada identidad, hace "reconnect with encryption"
}

// Limpiar bonding
ble_store_util_delete_all();

// Listar pares
ble_store_util_status_rr();  // Imprime en logs

3.3 LE Secure Connections vs Legacy

Legacy Pairing (BLE 4.0-4.1)

┌─────────────────────────────────────────┐
│ Vulnerabilidad: TK débil, algoritmo simple│
│ → KNOB attack: downgrade a 3-DES débil │
└─────────────────────────────────────────┘

LE Secure Connections (BLE 4.2+)

┌──────────────────────────────────────────────┐
│ ✓ ECDH robusta (P-256 curve)                │
│ ✓ Immune a KNOB (key length siempre 128 bit)│
│ ✓ Numeric Comparison sin vulnerabilidades   │
│ ✗ Bloquea ~160 ms durante pairing (ROM)     │
└──────────────────────────────────────────────┘

En ESP32, LESC es default. No la deshabilites a menos que requieras legacy.


3.4 Privacy: Dirección Privada Resolvible (RPA)

Problema

Un central puede trackear un periférico por su MAC fijo.

Solución: RPA (Resolvable Private Address)

Dirección de 48 bits generada dinámicamente:

┌─────────────┬──────────────────────┐
│ Hash (16 b) │ Random (32 b)        │
│ 2 MSB = 01  │ (lower 2 bits = 01)  │
└─────────────┴──────────────────────┘

Hash = AES(IRK, Random_part)[0:15]

  • Cada conexión genera RPA nueva.
  • Central con IRK bonded puede resolver dirección.
  • Sin IRK, imposible trackear.

En NimBLE:

// Forzar RPA (privacidad máxima)
ble_hs_cfg.rpa_timeout = 60;  // Regenerar cada 60 s
ble_hs_cfg.sm_use_rpa = 1;    // Use RPA en lugar de dirección fija

// En periférico
ble_gap_set_addr_type(BLE_ADDR_TYPE_RPA_RANDOM_DEFAULT);

3.5 Herramientas de Debugging de Seguridad

Extraer LTK de iOS (post-pairing)

  1. Xcode Bluetooth Packet Logger (Mac):

    # Busca: "LE Start Encryption"
    # En la expansión, observa: LTK = [16 bytes]
    
  2. Android HCI Snoop (developer options):

    adb pull /sdcard/btsnoop_hci.log
    # Abre en Wireshark, filtra por "Start Encryption"
    

Monitoreo en Tiempo Real (ESP32)

// Log pairing state
static void gatt_svr_subscribe_cb(struct ble_gap_event *event, void *arg) {
    if (event->type == BLE_GAP_EVENT_SUBSCRIBE) {
        ESP_LOGI("SECURITY", "Subscribe flags: 0x%04x", 
                 event->subscribe.cur_notify);
    }
}

// Log encryption state
case BLE_GAP_EVENT_ENC_CHANGE:
    ESP_LOGI("SECURITY", "Encrypted: %d",
             event->enc_change.encrypted);
    break;

3.6 Ejercicio Práctico 3: Periférico Seguro con Bonding

Objetivo: Crear un dispositivo que requiere LESC + Passkey, almacena bonding y re-encripta automáticamente.

Requisitos:

  1. NimBLE + LESC activado (default).
  2. Passkey Entry (periférico muestra código).
  3. Almacenamiento bonding en NVS.
  4. Característica protegida: requiere encriptación.
  5. Desconectar si intenta acceso sin encripción.

Pasos:

  1. Configura Security Manager:

    ble_hs_cfg.sm_mitm = 1;
    ble_hs_cfg.sm_bonding = 1;
    ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_DISP_ONLY;
    
  2. Implementa callback BLE_GAP_EVENT_PASSKEY_ACTION.

  3. En gatt_svr_chr_access, valida event->access.op y rechaza si no cifrado.

  4. Test: Parear, desconectar, reconectar sin re-introducir código.


3.7 Coexistencia WiFi-BLE y PSRAM

El Problema

ESP32 tiene un controller compartido para WiFi y BLE. Ambos usan la banda 2.4 GHz.

Collision mitigation:

  • Algoritmo de frequency hopping separa canales.
  • BLE tiene prioridad sobre WiFi en ciertos eventos críticos.
  • Aumenta latencia WiFi durante transferencias BLE.

Estrategia de Optimización

// Config: Asignar más tiempo a BLE si es crítico
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
bt_cfg.ble_ll_resolv_list_size = 0;  // Liberar RAM si no usas privacidad
esp_bt_controller_init(&bt_cfg);

// O usar PSRAM para buffers del stack
ble_hs_cfg.ble_l2cap_mtu = 251;  // Reduce MTU default si hay presión RAM

PSRAM (Pseudo SRAM)

ESP32 puede tener hasta 4 MB de PSRAM externo. Acceso es lento (2-5x que IRAM), pero útil para:

  • Buffers grandes.
  • Modelos ML pequeños.

En contexto de BLE:

  • No recomendable para buffers de paquetes (latencia).
  • Sí para almacenamiento de bonding LUTs si muchos pares.

3.8 ESP32 Memory Deep Dive

Layout de Memoria

IRAM (Instruction RAM): 192 KB total
├─ 64 KB: PRO/APP cache MMU
├─ 128 KB: Ejecutable + stack tasks + heap crítico
│  ├─ NimBLE task: ~8 KB stack
│  ├─ Application task: ~4 KB
│  └─ Heap: Resto

DRAM (Data RAM): 320 KB total
├─ 160 KB: Estáticos (.data + .bss)
├─ 160 KB: Heap runtime (rest)

FLASH (External): 4 MB típico
├─ Partition: app (1.2 MB código)
├─ Partition: nvs (64 KB bonding)
└─ Partition: rest (available)

Impacto en BLE

Stack IRAM DRAM Estático Heap Init
Bluedroid ~60 KB ~40 KB ~200 KB
NimBLE ~40 KB ~20 KB ~70 KB

Conclusión: NimBLE deja 130 KB más para ML models. Crítico para Edge AI.


DEBUGGING MASTERCLASS

Debugging 1: Logging Básico (Siempre Disponible)

ESP-IDF Monitor

idf.py -p /dev/ttyUSB0 monitor

Dentro del monitor:

# Cambiar nivel de log
Ctrl+T Ctrl+L → Choose log level → ble (default DEBUG)

# Reset
Ctrl+T Ctrl+R

# Salir
Ctrl+]

Filtrar Logs por Módulo

// En código
esp_log_level_set("BLE_GATT", ESP_LOG_DEBUG);  // Solo GATT debug
esp_log_level_set("*", ESP_LOG_INFO);          // Todo a INFO

// En menuconfig
idf.py menuconfig
 Component config  Log output  Default log level  DEBUG
 Bluetooth  Log level  Debug

Ejemplo de Logging Custom

#include "esp_log.h"

static const char *TAG = "MY_BLE_APP";

void my_function() {
    ESP_LOGD(TAG, "Debug: valor=%d", 42);
    ESP_LOGI(TAG, "Info: evento=%s", "connected");
    ESP_LOGW(TAG, "Warning: mtu bajo");
    ESP_LOGE(TAG, "Error: code=%d", error_code);
}

Debugging 2: GDB + OpenOCD (JTAG Hardware Required)

Hardware Setup

Requerimientos:

  • JTAG Adapter: ESP-PROG, Jlink, ST-Link, o Raspberry Pi.
  • 5 Cables: TDI, TDO, TCK, TMS, GND (+ SRST opcional).

ESP32 JTAG Pins:

GPIO12 → TDI
GPIO13 → TCK
GPIO14 → TDO
GPIO15 → TMS
GND    → GND

Instalación OpenOCD

# Linux (Debian/Ubuntu)
sudo apt-get install openocd

# macOS
brew install openocd

# Verify
openocd --version

Iniciar OpenOCD

# Para ESP-PROG en Linux
openocd -f interface/ftdi/esp32_devkitj_v1.cfg \
        -f target/esp32.cfg

# Para ST-Link en Windows
openocd -f interface/stlink.cfg \
        -f target/esp32.cfg

# Verás algo como:
# Info : Listening on port 3333 for gdb connections
# Info : Listening on port 6666 for telnet connections

Conectar GDB

Terminal 1 (OpenOCD ejecutándose):

openocd -f interface/esp32_prog.cfg -f target/esp32.cfg

Terminal 2 (GDB):

cd your_project_dir

# Opción 1: Automático (desde ESP-IDF)
idf.py gdb

# Opción 2: Manual
xtensa-esp32-elf-gdb build/your_app.elf

# En GDB console:
(gdb) target remote localhost:3333
(gdb) mon reset halt  # Halt on reset
(gdb) thb app_main    # Breakpoint temporal en app_main
(gdb) c               # Continue

Comandos GDB Útiles

# Breakpoints
(gdb) b function_name
(gdb) b file.c:123
(gdb) info breakpoints
(gdb) delete 1

# Stepping
(gdb) n                    # Next (step over)
(gdb) s                    # Step into function
(gdb) c                    # Continue
(gdb) finish               # Run until return

# Inspection
(gdb) p variable           # Print value
(gdb) p /x 0x1234          # Hex format
(gdb) x/32x 0x3ffb0000     # Examine memory (32 words hex)
(gdb) info registers       # All registers
(gdb) bt                   # Backtrace (stack)

# FreeRTOS-aware (custom GDB macros)
(gdb) info tasks           # List FreeRTOS tasks
(gdb) task 2               # Switch to task 2

Debugging BLE Specific Issues

Breakpoints en callbacks:

(gdb) b esp_gap_cb        # Breakpoint GAP callback
(gdb) b gatt_svr_chr_access  # Breakpoint GATT access

# Cuando se para, inspecciona evento
(gdb) p event->type       # Tipo de evento
(gdb) p event->connect.conn_handle  # Handle conexión

Debugging 3: Análisis de Paquetes (Wireshark + HCI Sniffing)

Opción A: Wireshark en Linux (sin hardware sniffer)

Limitación: Captura solo HCI local (advertiser), no conexiones activas.

# Instalar Wireshark + plugin Bluetooth
sudo apt-get install wireshark wireshark-plugin-btsnoop

# Ejecutar como root
sudo wireshark

# Capturar:
# 1. Capture → Interfaces
# 2. Selecciona "Bluetooth"
# 3. Start capturing

Filtros útiles:

btatt          # Filter: Attribute Protocol
btle           # Filter: BLE link layer
btatt.handle == 0x0003  # Characteristic handle
btatt.opcode == 0x0b    # Write command

Opción B: HCI Sniffing en Android

# Activar developer options
# → Bluetooth HCI snoop log: ON

# Ejecutar comando
adb pull /sdcard/btsnoop_hci.log

# Abrir en Wireshark
wireshark btsnoop_hci.log

Opción C: Captura de Logs de Sincronización (ESP32 Built-in)

# Compilar con debug habilitado
idf.py menuconfig
# → Component config → Bluetooth → Enable debug logs
# → Log Level → Debug

# En monitor, buscar:
# [HCI] LE Create Connection Complete
# [GATT] Write characteristic

Debugging 4: Profiling y Análisis de Performance

Task CPU Usage

#include "freertos/task.h"

void print_task_stats(void) {
    char* buffer = malloc(1024);
    vTaskList(buffer);
    printf("%s\n", buffer);
    free(buffer);
}

// Output:
// Task Name        State  Priority  Stack  Num
// nimble_host_task X      100       4096   123
// IDLE             R      0         1024   0

Heap Usage

#include "esp_heap_caps.h"

void print_heap_stats(void) {
    printf("Internal DRAM: %d bytes free\n",
           heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
    
    printf("Internal IRAM: %d bytes free\n",
           heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_32BIT));
}

Timing Analysis

#include "esp_timer.h"

void measure_operation(void) {
    int64_t start = esp_timer_get_time();  // microseconds
    
    // [Operation]
    
    int64_t elapsed_us = esp_timer_get_time() - start;
    ESP_LOGI(TAG, "Operation took %.2f ms", elapsed_us / 1000.0);
}

Debugging 5: Clang + AddressSanitizer (ASAN)

Enable AddressSanitizer

idf.py menuconfig
# → Component config → Compiler options → Enable AddressSanitizer

Build & Run

idf.py build flash monitor

Output de memory error:

=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on unknown address
    Read of size 4 at 0x... Thread T1
    #0 in some_function ...

Instrumentación Manual

#include "sanitizer/asan_interface.h"

void unsafe_access(void) {
    uint8_t buffer[10];
    
    // Marcar rango válido
    ASAN_POISON_MEMORY_REGION(buffer + 10, 1);
    
    // Esto triggea ASAN error si accede
    buffer[15] = 0xFF;  // Out of bounds!
}

EJERCICIOS PRÁCTICOS PROGRESIVOS

Ejercicio 1: Beacon Personalizado (Nivel: Básico)

Objetivo: Dominar advertising y formatos de datos.

Requisitos:

  • Transmitir temperatura simulada cada 100 ms.
  • Usar Service Data con UUID 16-bit personalizado (0xAABB).
  • Formato: [temp_byte] [humidity_byte].
  • Verificable en nRF Connect.

Template:

static uint8_t service_data[] = {
    0x02, 0x0A, 0xF4,           // TX Power
    0x07, 0x16,                 // Service Data 16-bit
    0xBB, 0xAA,                 // UUID 0xAABB
    25,                         // Temperature
    60                          // Humidity
};

// Update cada 100ms en timer
void update_adv_data(void) {
    service_data[4] = read_temp();
    service_data[5] = read_humidity();
    ble_gap_adv_set_fields(&adv_fields);
}

Verificación:

  1. Conecta a nRF Connect.
  2. Abre Raw Data en advertising.
  3. Confirma que hex cambia cada 100 ms.

Ejercicio 2: GATT Server Interactivo (Nivel: Intermedio)

Objetivo: Implementar lectura/escritura de características + notificaciones.

Requisitos:

  • Servicio custom (UUID 128-bit).
  • 3 características:
    1. LED Control (write): 0x00=OFF, 0x01=ON.
    2. Button State (read+notify): 0x00/0x01.
    3. BPM Mock (read+notify): Valor 60-100 BPM.
  • BPM actualiza cada 2 segundos.
  • Button emula presión cada 5 segundos.

Pasos:

  1. Define UUID 128-bit de servicio:

    #define CUSTOM_SERVICE_UUID \
        {0x12, 0x34, 0x56, 0x78, ...}  // 16 bytes
    
  2. Implementa task que actualiza BPM y Button.

  3. Maneja GATT_WRITE_EVT para LED control.

  4. En nRF Connect, verifica:

    • Write LED → GPIO on/off.
    • Read BPM → valor entre 60-100.
    • Suscribirse a BPM → recibe notificaciones cada 2s.

Ejercicio 3: Bonding + Security (Nivel: Avanzado)

Objetivo: Dominar seguridad LESC con persistencia.

Requisitos:

  • LESC + Numeric Comparison.
  • Almacenamiento bonding en NVS.
  • Reinicio sin re-pairing.
  • Rechazo de acceso no encriptado.

Pasos:

  1. Configura SM:

    ble_hs_cfg.sm_mitm = 1;
    ble_hs_cfg.sm_bonding = 1;
    ble_hs_cfg.sm_sc = 1;  // Secure Connections
    
  2. Implementa passkey callback:

    case BLE_GAP_EVENT_PASSKEY_ACTION:
        ESP_LOGI("SEC", "Passkey: %06d", 
                 event->passkey.params.action_type);
        break;
    
  3. Valida encriptación en GATT access:

    if (!ble_gap_conn_find(conn_handle, &desc)) {
        if (!desc.sec_state.encrypted) {
            return BLE_ATT_ERR_INSUFFICIENT_AUTHEN;
        }
    }
    
  4. Test:

    • Parear, confirmar passkey.
    • Reboot, reconectar → sin passkey.
    • Borrar bonding en ESP32 (ble_store_util_delete_all()).
    • Reconectar → pide passkey nuevamente.

Ejercicio 4: IoT Sensor Sync (Nivel: Avanzado+)

Objetivo: Integrar múltiples sensores + WiFi + BLE.

Requisitos:

  • 1x Beacon BLE (temperatura).
  • 1x GATT Server (BLE) con:
    • Lectura ADC (temperatura real).
    • Lectura RSSI de WiFi.
    • Reset button.
  • WiFi conectado a broker MQTT.
  • Publicar datos por MQTT cada 10s.
  • Recibir comandos por GATT + aplicar.

Pasos:

  1. Setup ADC + WiFi + MQTT.

  2. Crea beacon con temp actualizada.

  3. Crea GATT server con:

    • Service: Environmental Sensing (0x181A).
    • Characteristic: Temperature (0x2A6E).
    • Characteristic: Humidity (mock, 0x2A6F).
  4. Escribe datos en NVS bajo etiqueta iot_config.

  5. Implementa: Write characteristic para actualizar MQTT topic.

  6. Test:

    • BLE beacon muestra temp en tiempo real.
    • GATT server lee sensores.
    • MQTT publica cada 10s.
    • Comando BLE actualiza MQTT topic.

REFERENCIAS & RECURSOS

Especificaciones Oficiales

  • Bluetooth Core Specification Vol 4 (BLE)

    • Part A: Architecture
    • Part B: Link Layer
    • Part D: ATT/GATT
    • Part E: HCI
    • Part H: Security
  • ESP32 Technical Reference Manual

    • Wireless, Memory, Timers

Documentación

Herramientas de Desarrollo

  • nRF Connect (mobile app): Escanear, conectar, debug BLE
  • Wireshark: Análisis de paquetes BLE/HCI
  • OpenOCD: JTAG debugger
  • GDB: GNU Debugger
  • Ubertooth One: Hardware BLE sniffer (avanzado)

Repositorios Open-Source

  • esp-idf/examples/bluetooth: Ejemplos oficiales
  • apache/mynewt-nimble: Stack NimBLE
  • espressif/esp-idf: Full ESP-IDF
  • h2zero/NimBLE-Arduino: NimBLE para Arduino IDE

Artículos Técnicos Recomendados

  1. "The Bluetooth Low Energy Protocol Stack: Understanding the Fundamentals" - NoveBits
  2. "A Practical Guide to Debugging BLE Communication on iOS" - NoveBits
  3. "Understanding the Architecture of the Bluetooth Low Energy Protocol" - Analog Devices
  4. "BLE Frequency Hopping Algorithm Analysis" - arxiv.org (si te interesa sniffing avanzado)

Comunidades

  • ESP32 Community Forum: esp32.com
  • Apache NimBLE Mailing List: dev@mynewt.apache.org
  • Bluetooth SIG: bluetooth.com (especificaciones)

ROADMAP: Próximos Pasos para Edge AI + IoT + Blockchain

Con BLE + ESP32 dominados, el siguiente paso en tu roadmap:

  1. Quantización de Modelos ML

    • TensorFlow Lite Micro (< 1 MB, ~10 ms inferencia).
    • Desplegar en ESP32 con BLE para actualizaciones federadas.
  2. Federated Learning sobre BLE

    • Múltiples ESP32 (sensores) entrenando modelo compartido.
    • Coordinación por NimBLE + Bluetooth Mesh.
  3. Blockchain + BLE

    • Verificación de datos IoT en cadena (Substrate).
    • Provenance de sensor data.
  4. Edge Inference Optimization

    • ONNX Runtime en ESP32.
    • Comparación: nanoGPT vs DistilBERT en hardware limitado.
  5. Rust + CUDA para kernels críticos

    • Compilar NimBLE components en Rust (safety).
    • CUDA kernels para training federated en nodos GPU (coordinadores).

¡Felicidades por completar esta masterclass! Ahora eres experto en BLE + ESP32. 🎓

Para profundizar, revisa los repos sugeridos e implementa los mini-proyectos. Cada iteración refuerza internalizacion de conceptos.