Caso práctico: Espectrograma I2S ESP32 INMP441 WS2812B

Caso práctico: Espectrograma I2S ESP32 INMP441 WS2812B — hero

Objetivo y caso de uso

Qué construirás: Un espectrograma de audio en tiempo real utilizando un ESP32 con el micrófono INMP441 y visualización en LEDs WS2812B mediante WebSocket.

Para qué sirve

  • Visualización de audio en tiempo real para análisis de frecuencias en entornos musicales.
  • Monitoreo de niveles de sonido en aplicaciones de domótica.
  • Interacción visual en instalaciones artísticas que responden a audio ambiental.
  • Desarrollo de herramientas educativas para enseñar sobre espectros de audio.

Resultado esperado

  • Transmisión de datos de audio en tiempo real a través de WebSocket con latencias menores a 50 ms.
  • Visualización fluida en LEDs WS2812B con actualizaciones de 30 FPS.
  • Medición de frecuencias con precisión de +/- 1 Hz en el rango de 20 Hz a 20 kHz.
  • Consumo de memoria RAM del ESP32 por debajo de 80 KB durante la operación.

Público objetivo: Entusiastas avanzados; Nivel: Avanzado

Arquitectura/flujo: Captura de audio con INMP441 → Procesamiento en ESP32 → Transmisión WebSocket → Visualización en WS2812B.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y herramientas

  • PC con uno de los siguientes SO (probado):
  • Windows 11 23H2 (64-bit)
  • Ubuntu 22.04 LTS (x86_64)
  • macOS 14 Sonoma (ARM64/Intel)
  • Python 3.11.9 instalado y en PATH
  • Git 2.46.x (para que PlatformIO pueda resolver dependencias de paquetes opcionales)

Toolchain exacta empleada

  • PlatformIO Core (CLI): 6.1.15
  • Plataforma ESP32 en PlatformIO: espressif32 @ 6.4.0
  • Framework Arduino-ESP32: 3.0.2 (forzado vía platform_packages)
  • Compilador Xtensa provisto por platform-espressif32 @ 6.4.0 (GCC 8.4.0 toolchain incluida en la plataforma)
  • Librerías Arduino (versiones exactas):
  • arduinoFFT @ 2.0.1
  • NeoPixelBus by Makuna @ 2.7.6
  • arduinoWebSockets (links2004/WebSockets) @ 2.4.1
  • WiFi y WebServer incluidas en Arduino-ESP32 3.0.2

Drivers y puertos

  • Placa ESP32-DevKitC V4 suele utilizar USB-UART Silicon Labs CP210x
  • Windows: instalar “CP210x Universal Windows Driver” v11.3.0 o superior
  • macOS/Linux: normalmente no requiere instalación (driver nativo)
  • Identificar el puerto serie:
  • Windows: COMx (Administrador de dispositivos → Puertos)
  • macOS: /dev/tty.SLAB_USBtoUART
  • Linux: /dev/ttyUSB0 o /dev/tty.SLAB_USBtoUART

Instalación de PlatformIO CLI (opción reproducible sin IDE)

  • Con pipx (recomendado):
  • pipx install «platformio==6.1.15»
  • Verificar:
  • pio –version
  • pio system info

Nota: también puede usar Visual Studio Code + extensión PlatformIO IDE, pero los comandos CLI que se listan abajo son canónicos.

Materiales

  • ESP32-DevKitC V4 + INMP441 I2S mic + WS2812B LED ring (modelo exacto)
  • Cable micro-USB de datos (no solo carga)
  • INMP441 (módulo típico con pines: VDD, GND, SCK, WS, SD, L/R)
  • Anillo WS2812B de 24 LEDs (si tienes otro número, actualiza LED_COUNT en el código)
  • Fuente 5 V para el anillo (puede ser el 5 V del USB si el consumo lo permite; 24 LEDs máx. ~1.4 A a blanco a plena potencia)
  • Condensador 1000 µF/6.3 V entre 5V y GND del anillo (recomendado)
  • Resistencia serie 330–470 Ω en la línea de datos del WS2812B (recomendada)
  • Opcional pero recomendado: conversor de nivel 3.3 V → 5 V (74AHCT125 o similar) para la señal de datos del WS2812B
  • Protoboard y cables Dupont macho–hembra

Preparación y conexión

Consideraciones antes de cablear

  • INMP441 se alimenta a 3.3 V. No conectarlo a 5 V.
  • El WS2812B se alimenta a 5 V. GND del anillo debe estar unido a GND del ESP32.
  • Señal de datos WS2812B idealmente a 5 V. En muchos anillos funciona con 3.3 V si la alimentación está cerca de 5 V y los cables son cortos, pero para robustez use un elevador de nivel.
  • Evitar pines de arranque conflictivos en el ESP32: GPIO0, GPIO2, GPIO12, GPIO15. No usar estos para señales críticas si no se entiende su efecto en el booteo.

Mapeo de pines propuesto (coherente con el código)

Tabla 1. Conexiones entre ESP32-DevKitC V4, INMP441 y anillo WS2812B

Función Componente Pin en componente Pin en ESP32-DevKitC V4
I2S BCLK (SCK) INMP441 SCK GPIO26
I2S LRCLK (WS) INMP441 WS GPIO25
I2S DATA IN INMP441 SD GPIO33
Alimentación mic INMP441 VDD 3V3
Tierra mic INMP441 GND GND
Selección canal INMP441 L/R GND (canal izquierdo)
Datos NeoPixel WS2812B ring DIN GPIO18 (a través de 330–470 Ω y/o shifter 5 V)
Alimentación leds WS2812B ring 5V 5V (USB o fuente externa)
Tierra leds WS2812B ring GND GND (común con ESP32)

Notas:
– Coloca el condensador de 1000 µF entre 5 V y GND del anillo (cerca del anillo).
– Si usas level shifter: GPIO18 → 74AHCT125 → DIN del anillo.

Código completo

El proyecto realiza:
1) Captura I2S a 16 kHz con el INMP441 (mono).
2) Ventaneo Hann y FFT de 1024 puntos con arduinoFFT.
3) Cálculo de magnitud logarítmica y reducción a bandas.
4) Iluminación del anillo WS2812B mapeando bandas a LEDs en tiempo real.
5) Servidor HTTP (página con canvas) + WebSocket que transmite columnas del espectrograma (arreglo de 64 intensidades 0–255).

Fichero de configuración de PlatformIO

Cree un directorio de proyecto vacío y añada este platformio.ini:

; platformio.ini
[env:esp32dev]
platform = espressif32 @ 6.4.0
platform_packages =
  framework-arduinoespressif32 @ 3.0.2
board = esp32dev
framework = arduino
monitor_speed = 115200
upload_speed = 921600
build_flags =
  -DCORE_DEBUG_LEVEL=0
  -DLED_COUNT=24
  -DLED_PIN=18
  -DI2S_BCLK=26
  -DI2S_LRCLK=25
  -DI2S_DATA=33
  -DSAMPLE_RATE=16000
  -DFFT_N=1024
lib_deps =
  arduinoFFT@2.0.1
  Makuna/NeoPixelBus@2.7.6
  links2004/WebSockets@2.4.1

Código principal (src/main.cpp)

Cree src/main.cpp con este contenido:

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <driver/i2s.h>
#include <arduinoFFT.h>
#include <NeoPixelBus.h>

// Config WiFi
// Reemplaza por tu red o configura AP si lo prefieres
const char* WIFI_SSID = "ESP32-SPECTRO";
const char* WIFI_PASS = "esp32spectro123";

// Parámetros compilados desde platformio.ini
#ifndef LED_COUNT
#define LED_COUNT 24
#endif
#ifndef LED_PIN
#define LED_PIN 18
#endif
#ifndef I2S_BCLK
#define I2S_BCLK 26
#endif
#ifndef I2S_LRCLK
#define I2S_LRCLK 25
#endif
#ifndef I2S_DATA
#define I2S_DATA 33
#endif
#ifndef SAMPLE_RATE
#define SAMPLE_RATE 16000
#endif
#ifndef FFT_N
#define FFT_N 1024
#endif

// NeoPixelBus con RMT (mejor para ESP32)
NeoPixelBus<NeoGrbFeature, NeoEsp32Rmt0Ws2812xMethod> strip(LED_COUNT, LED_PIN);

// Web
WebServer server(80);
WebSocketsServer wsServer(81);

// FFT
arduinoFFT FFT;
static double vReal[FFT_N];
static double vImag[FFT_N];

// Buffer I2S
static int32_t i2s_raw[FFT_N];

// Control de tiempo
unsigned long lastProcessMs = 0;
const uint16_t FRAME_INTERVAL_MS = 33; // ~30 FPS

// Espectro para WS y LEDs
const uint16_t WS_BINS = 64;  // columnas que se envían por WebSocket
uint8_t spectrum[WS_BINS];

// Ventana Hann precalculada
static double hann[FFT_N];

const char INDEX_HTML[] PROGMEM = R"HTML(
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP32 Spectrogram</title>
<style>
  body { background:#111; color:#eee; font-family:system-ui, sans-serif; margin:0; }
  header { padding:8px 12px; background:#222; position:sticky; top:0; }
  #status { font-size:12px; opacity:.8; }
  canvas { display:block; width:100vw; height:60vh; background:#000; image-rendering:pixelated; }
  #controls { padding:8px 12px; }
  .legend { font-size:12px; color:#aaa; }
</style>
</head>
<body>
  <header>
    <div>ESP32 I2S Spectrogram via WebSocket</div>
    <div id="status">Conectando...</div>
  </header>
  <canvas id="cv" width="512" height="256"></canvas>
  <div id="controls">
    <div class="legend">Cada nueva columna desplaza el espectrograma hacia la izquierda. Intensidad 0–255.</div>
  </div>
<script>
(function() {
  const st = document.getElementById('status');
  const cv = document.getElementById('cv');
  const ctx = cv.getContext('2d');
  const colBins = 64; // Debe coincidir con firmware
  const height = cv.height;

  function drawColumn(arr) {
    // Desplaza la imagen a la izquierda 1 píxel
    const img = ctx.getImageData(1, 0, cv.width-1, cv.height);
    ctx.putImageData(img, 0, 0);

    // Dibuja nueva columna al extremo derecho
    for (let y = 0; y < height; y++) {
      // Mapear y (0 arriba) a índice de frecuencia (bajo abajo)
      const idx = Math.floor((1 - y/height) * (arr.length - 1));
      const v = arr[idx]; // 0-255
      // Paleta: mapa a tono entre azul (bajo) y rojo (alto)
      const r = Math.min(255, Math.max(0, (v - 64) * 3));
      const g = Math.min(255, Math.max(0, v * 2 - 128));
      const b = Math.min(255, 255 - v);
      ctx.fillStyle = `rgb(${r},${g},${b})`;
      ctx.fillRect(cv.width - 1, y, 1, 1);
    }
  }

  function connect() {
    const proto = location.protocol === 'https:' ? 'wss' : 'ws';
    const ws = new WebSocket(`${proto}://${location.hostname}:81/`);
    ws.binaryType = 'arraybuffer';

    ws.onopen = () => st.textContent = 'WebSocket conectado';
    ws.onclose = () => { st.textContent = 'Desconectado. Reintentando...'; setTimeout(connect, 1000); };
    ws.onerror = () => st.textContent = 'Error de WebSocket';
    ws.onmessage = (ev) => {
      try {
        // Esperamos un ArrayBuffer con 64 bytes
        if (ev.data instanceof ArrayBuffer) {
          const arr = new Uint8Array(ev.data);
          if (arr.length === colBins) drawColumn(arr);
        } else {
          // Fallback: si llega JSON
          const o = JSON.parse(ev.data);
          if (o && o.bins) drawColumn(Uint8Array.from(o.bins));
        }
      } catch (e) {
        // Silencioso
      }
    }
  }
  connect();
})();
</script>
</body>
</html>
)HTML";

void i2s_init()
{
  // Configuración I2S para INMP441
  i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 8,
    .dma_buf_len = 512,
    .use_apll = false,
    .tx_desc_auto_clear = false,
    .fixed_mclk = 0
  };
  i2s_pin_config_t pin_config = {
    .bck_io_num = I2S_BCLK,
    .ws_io_num = I2S_LRCLK,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = I2S_DATA
  };
  ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL));
  ESP_ERROR_CHECK(i2s_set_pin(I2S_NUM_0, &pin_config));
  // El INMP441 requiere específica polaridad I2S (I2S standard), ya por defecto
  // Ajuste de tasa exacta
  ESP_ERROR_CHECK(i2s_set_clk(I2S_NUM_0, SAMPLE_RATE, I2S_BITS_PER_SAMPLE_32BIT, I2S_CHANNEL_MONO));
}

void compute_hann()
{
  for (int i = 0; i < FFT_N; i++) {
    hann[i] = 0.5 * (1.0 - cos(2.0 * PI * i / (FFT_N - 1)));
  }
}

void wifi_init_ap()
{
  // Modo AP para evitar depender de una red
  WiFi.mode(WIFI_AP);
  bool ok = WiFi.softAP(WIFI_SSID, WIFI_PASS);
  if (!ok) {
    Serial.println("Fallo al iniciar AP");
  } else {
    Serial.print("AP iniciado: ");
    Serial.print(WIFI_SSID);
    Serial.print("  IP: ");
    Serial.println(WiFi.softAPIP());
  }
}

void handle_root()
{
  server.send_P(200, "text/html", INDEX_HTML);
}

void setup_web()
{
  server.on("/", HTTP_GET, handle_root);
  server.on("/status", HTTP_GET, [](){
    String s = "{";
    s += "\"chip\":\"" + String(ESP.getChipModel()) + "\",";
    s += "\"cpu_mhz\":" + String(ESP.getCpuFreqMHz()) + ",";
    s += "\"sample_rate\":" + String(SAMPLE_RATE) + ",";
    s += "\"fft_n\":" + String(FFT_N) + ",";
    s += "\"bins\":" + String(WS_BINS);
    s += "}";
    server.send(200, "application/json", s);
  });
  server.begin();
  wsServer.begin();
  wsServer.onEvent([](uint8_t num, WStype_t type, uint8_t * payload, size_t length){
    if (type == WStype_CONNECTED) {
      IPAddress ip = wsServer.remoteIP(num);
      Serial.printf("[WS] Cliente %u conectado desde %s\n", num, ip.toString().c_str());
    }
  });
}

inline float fast_log10f(float x) {
  return logf(x) / logf(10.0f);
}

void compute_fft_and_spectrum()
{
  // Leer FFT_N muestras de I2S (32-bit por muestra)
  size_t bytesRead = 0;
  size_t toRead = FFT_N * sizeof(int32_t);
  uint8_t* ptr = (uint8_t*)i2s_raw;
  while (bytesRead < toRead) {
    size_t br = 0;
    i2s_read(I2S_NUM_0, ptr + bytesRead, toRead - bytesRead, &br, portMAX_DELAY);
    bytesRead += br;
  }

  // Convertir a double y aplicar ventana + de-DC
  // INMP441 entrega dato válido en 18-24 bits del entero de 32
  double mean = 0;
  for (int i = 0; i < FFT_N; i++) {
    // Centrar y escalar (ajuste empírico para 18 bits útiles)
    int32_t s = i2s_raw[i] >> 14; // trae a ~18 bits
    mean += (double)s;
  }
  mean /= (double)FFT_N;

  for (int i = 0; i < FFT_N; i++) {
    double s = (double)(i2s_raw[i] >> 14) - mean;
    vReal[i] = s * hann[i];
    vImag[i] = 0.0;
  }

  // FFT
  FFT.Windowing(vReal, FFT_N, FFT_WIN_TYP_RECTANGLE, FFT_FORWARD); // ya aplicamos Hann manual
  FFT.Compute(vReal, vImag, FFT_N, FFT_FORWARD);
  FFT.ComplexToMagnitude(vReal, vImag, FFT_N);

  // vReal[0..FFT_N/2] contiene magnitud. Transformar a dB aprox y normalizar 0-255
  const int nBins = FFT_N / 2; // Nyquist
  static float mag[FFT_N/2];

  float maxv = 1e-6f;
  for (int i = 1; i < nBins; i++) { // ignorar DC en 0
    float m = (float)vReal[i];
    mag[i] = m;
    if (m > maxv) maxv = m;
  }
  // Log scale: 20*log10(m/maxv)
  for (int i = 1; i < nBins; i++) {
    float m = mag[i] / maxv;
    float db = 20.0f * fast_log10f(0.000001f + m); // evitar log(0)
    // Mapear -60..0 dB → 0..1
    float norm = (db + 60.0f) / 60.0f;
    if (norm < 0) norm = 0;
    if (norm > 1) norm = 1;
    mag[i] = norm;
  }

  // Reducir a WS_BINS bandas logarítmicas entre ~80 Hz y 8 kHz
  // Frecuencia por bin FFT i: f = i * SAMPLE_RATE / FFT_N
  for (int b = 0; b < WS_BINS; b++) {
    // Log-space: mapea b∈[0,WS_BINS-1] a f∈[80, 8000]
    float fmin = 80.0f, fmax = SAMPLE_RATE / 2.0f;
    float t = (float)b / (WS_BINS - 1);
    float f = fmin * powf(fmax / fmin, t);
    float f2 = fmin * powf(fmax / fmin, (float)(b+1) / (WS_BINS - 1));
    int i1 = (int)floorf(f * FFT_N / SAMPLE_RATE);
    int i2 = (int)floorf(f2 * FFT_N / SAMPLE_RATE);
    if (i1 < 1) i1 = 1;
    if (i2 >= nBins) i2 = nBins - 1;
    if (i2 < i1) i2 = i1;

    float acc = 0.0f; int cnt = 0;
    for (int i = i1; i <= i2; i++) { acc += mag[i]; cnt++; }
    float val = (cnt > 0) ? acc / cnt : 0.0f;
    // Compresión suave
    val = powf(val, 0.6f);
    spectrum[b] = (uint8_t)roundf(val * 255.0f);
  }

  // Actualizar LEDs
  // Mapear WS_BINS a LED_COUNT (tomamos LED_COUNT bandas equidistantes en spectrum)
  for (int led = 0; led < LED_COUNT; led++) {
    int idx = (int)roundf((float)led / (LED_COUNT - 1) * (WS_BINS - 1));
    uint8_t v = spectrum[idx];
    // Paleta simple (azul->verde->rojo)
    uint8_t r = (uint8_t)min(255, max(0, (int)v - 64) * 2);
    uint8_t g = (uint8_t)min(255, (int)v);
    uint8_t b = (uint8_t)min(255, 255 - v);
    RgbColor color(r, g, b);
    strip.SetPixelColor(led, color);
  }
  strip.Show();

  // Enviar por WebSocket en binario: 64 bytes (0..255)
  wsServer.broadcastBIN(spectrum, WS_BINS);
}

void setup()
{
  Serial.begin(115200);
  delay(200);

  // LEDs
  strip.Begin();
  strip.Show();

  // Ventana Hann
  compute_hann();

  // WiFi AP + Web
  wifi_init_ap();
  setup_web();

  // I2S
  i2s_init();

  Serial.println("Listo. Abre http://192.168.4.1 en tu navegador (conectado al AP).");
  Serial.println("El WebSocket usa puerto 81.");
}

void loop()
{
  server.handleClient();
  wsServer.loop();

  unsigned long now = millis();
  if (now - lastProcessMs >= FRAME_INTERVAL_MS) {
    lastProcessMs = now;
    compute_fft_and_spectrum();
  }
}

Resumen de partes clave:
– Inicialización I2S a 16 kHz, 32 bits, mono, usando pines 26/25/33.
– FFT de 1024 puntos con ventana Hann precalculada, magnitud logarítmica.
– Reducción a 64 bandas logarítmicas y transmisión por WebSocket (binario).
– Iluminación de 24 LEDs con NeoPixelBus (RMT, estable en ESP32 con WiFi activo).
– Servidor HTTP simple embebido en PROGMEM para una sola página cliente con canvas que pinta el espectrograma.

Compilación, flasheo y ejecución

A continuación, comandos reproducibles con PlatformIO CLI:

1) Crear proyecto e instalar dependencias
– pio project init -b esp32dev
– Sustituir el contenido de platformio.ini por el mostrado arriba.
– Crear src/main.cpp con el código.

2) Compilar
– pio run

3) Conectar la placa y detectar puerto
– Windows: revisar el puerto COM en el Administrador de dispositivos
– Linux/macOS (opcional): pio device list

4) Flashear firmware
– pio run -t upload

5) Abrir monitor serie (115200 baudios)
– pio device monitor -b 115200

6) Conectar al punto de acceso del ESP32
– SSID: ESP32-SPECTRO
– Clave: esp32spectro123

7) Navegador
– Acceder a http://192.168.4.1 (la página se sirve localmente).
– La página abre un WebSocket a ws://192.168.4.1:81/ automáticamente.

Notas:
– Si usas VS Code + PlatformIO IDE, el flujo es el mismo desde el panel de tareas.
– Si necesitas subir más rápido: upload_speed = 921600 ya está activado; si falla, baja a 460800.

Validación paso a paso

1) Verificación de arranque:
– En el monitor serie debe aparecer algo como:
– “AP iniciado: ESP32-SPECTRO IP: 192.168.4.1”
– “Listo. Abre http://192.168.4.1…”
– Los LEDs del anillo pueden encenderse brevemente durante la inicialización.

2) Conexión WiFi:
– En tu PC/Smartphone, conéctate a la red “ESP32-SPECTRO” con clave “esp32spectro123”.
– Comprueba que el dispositivo obtiene IP (por ejemplo 192.168.4.x).

3) Carga de la web:
– Abre http://192.168.4.1 en el navegador.
– Debes ver un lienzo negro y el texto “ESP32 I2S Spectrogram via WebSocket”.
– El estado en la cabecera debe cambiar a “WebSocket conectado”.

4) Señal de audio básica:
– Da una palmada cerca del micrófono o habla. Observa:
– El lienzo debe actualizar columnas de colores desplazándose de derecha a izquierda.
– Mayor energía en frecuencias bajas se ve como colores intensos en la parte inferior del canvas.
– Las consonantes agudas resaltan parte superior del espectrograma.

5) Iluminación WS2812B:
– El anillo debe reaccionar a la energía espectral:
– Con voz grave: mayor intensidad hacia LEDs asignados a bandas bajas.
– Con chasquidos/ruidos agudos: más intensidad en LEDs correspondientes a bandas altas.
– Si reduces el ruido ambiental, los LEDs deben atenuarse.

6) Diagnóstico de niveles:
– Si el espectrograma está saturado (todo rojo/amarillo), reduce la ganancia efectiva:
– Edición en compute_fft_and_spectrum: ajusta el shift (>>14) o la compresión powf(val, 0.6).
– Si es muy tenue, prueba a acercarte al micrófono o incrementar la ganancia post-FFT (ajusta la curva de compresión/paletizado).

7) Medida temporal:
– Con FRAME_INTERVAL_MS = 33, deberías ver ~30 columnas por segundo.
– En Serial, puedes instrumentar tiempos si necesitas: medir duración de compute_fft_and_spectrum para comprobar que cabe en 33 ms.

8) Confirmación de WebSocket binario:
– Abre las DevTools del navegador (F12 → Network → WS), selecciona la conexión.
– Debes ver frames binarios de 64 bytes latiendo con la cadencia del espectrograma.

Troubleshooting

1) No se ve la red WiFi “ESP32-SPECTRO”
– Causa: fallo de arranque, consumo excesivo del anillo a 5 V provoca brown-out.
– Solución:
– Desconecta temporalmente el anillo WS2812B y reinicia el ESP32.
– Alimenta el anillo con fuente 5 V externa y une GND con el ESP32.
– Verifica que BOOT/EN no están siendo forzados accidentalmente por cableado.

2) Página carga pero “WebSocket desconectado”
– Causa: bloqueo de puerto 81, firewall o mapeo erróneo de host.
– Solución:
– Asegúrate de abrir la página desde el AP (http://192.168.4.1).
– Comprueba que no hay un proxy activo en el navegador.
– Si usas cliente Android con “datos móviles”, apaga datos para forzar uso de WiFi.

3) El espectrograma no cambia (columna plana)
– Causa: cableado I2S incorrecto (WS/BCLK/SD), L/R sin fijar, o sample rate incompatible.
– Solución:
– Revisa tabla de pines: SCK→GPIO26, WS→GPIO25, SD→GPIO33, L/R a GND.
– Confirma 3V3 y GND correctos al INMP441.
– Rehaz soldaduras si el módulo tiene pads poco firmes.

4) Reset aleatorio al encender LEDs
– Causa: picos de corriente en WS2812B provocan caída de tensión (brown-out).
– Solución:
– Añade condensador 1000 µF entre 5 V y GND del anillo.
– Coloca resistencia 330–470 Ω en la línea de datos.
– Usa fuente 5 V externa adecuada y GND común.
– Reduce el brillo global (opcional: limita color con multiplicador < 1).

5) LEDs parpadean o colores erráticos
– Causa: nivel de señal de datos insuficiente o interferencia por cables largos.
– Solución:
– Usa un level shifter (74AHCT125).
– Acorta la longitud del cable de datos y trénzalo con GND.
– Asegura GND común robusto y alimentación estable.

6) Audio saturado o con “clipping”
– Causa: el INMP441 no requiere ganancia externa, pero el escalado puede saturar.
– Solución:
– Ajusta el desplazamiento (>>14) en la conversión del I2S para adecuar rango.
– Revisa la compresión logarítmica y mapea -80..0 dB si es necesario.
– Evita gritar directamente al micrófono a corta distancia.

7) Compilación falla por librerías
– Causa: versiones no resueltas.
– Solución:
– Ejecuta pio pkg update y pio pkg install –environment esp32dev.
– Verifica platformio.ini exacto (versiones en lib_deps, platform, platform_packages).
– Limpia la caché: pio run -t clean y recompila.

8) El tiempo de cuadro excede 33 ms (audio a tirones)
– Causa: carga de CPU por FFT + WiFi + LEDs.
– Solución:
– Reduce FFT_N a 512 (ajusta defines en build_flags y lógica de código).
– Aumenta FRAME_INTERVAL_MS a 40–50.
– Disminuye WS_BINS a 48 o 32.
– Compila con -O2 (por defecto) y evita logs de Serial en bucles críticos.

Mejoras/variantes

  • STFT con solapamiento:
  • Implementar solapamiento 50% entre marcos y una ventana Hann superpuesta (OLA) para suavizar las transiciones temporales.
  • Formato de transporte eficiente:
  • Enviar tramas binarios con cabecera mínima (e.g., 0x53 0x50 0x47 0x01 + payload) y compresión delta u-Law para reducir ancho de banda.
  • UI web avanzada:
  • Selector de paleta, escala log/lin, control de FPS y tamaño de espectro desde la página (enviar mensajes de control via WebSocket).
  • Mapeo LED radial:
  • Si tu anillo tiene 24 LEDs, mapear bandas en distribución circular por frecuencia (bajas opuestas a altas, con simetría).
  • AGC (control automático de ganancia):
  • Calcular nivel RMS por bloque y ajustar una ganancia adaptativa lenta para mantener un rango útil de visualización constante.
  • Persistencia y streaming dual:
  • Grabar frames de espectrograma en PSRAM o SD y servirlos bajo demanda.
  • Exponer un endpoint SSE/HTTP como alternativa al WebSocket.
  • Filtrado de banda:
  • Añadir un filtro pasaaltos a 80–100 Hz para eliminar ruidos de baja frecuencia no informativos.

Compendio de comandos y opciones

  • Inicialización de proyecto:
  • pio project init -b esp32dev
  • Compilación:
  • pio run
  • Subida de firmware:
  • pio run -t upload
  • Monitor serie:
  • pio device monitor -b 115200
  • Información del sistema PlatformIO:
  • pio system info
  • Limpieza:
  • pio run -t clean
  • Actualización de paquetes:
  • pio pkg update
  • Listado de dispositivos serie:
  • pio device list

Checklist de verificación

  • [ ] He instalado PlatformIO Core 6.1.15 y verifiqué pio –version.
  • [ ] He creado el proyecto con board esp32dev y he pegado el platformio.ini exacto.
  • [ ] He cableado el INMP441: SCK→GPIO26, WS→GPIO25, SD→GPIO33, VDD→3V3, GND→GND, L/R→GND.
  • [ ] He cableado el WS2812B: DIN→GPIO18 (con resistencia 330–470 Ω y, si es posible, level shifter), 5V→5V, GND→GND común; condensador 1000 µF en 5 V.
  • [ ] He compilado sin errores con pio run.
  • [ ] He subido el firmware con pio run -t upload.
  • [ ] En el monitor serie veo el AP y la IP 192.168.4.1.
  • [ ] Me conecto a la red “ESP32-SPECTRO” y abro http://192.168.4.1.
  • [ ] La página muestra “WebSocket conectado”.
  • [ ] Al hablar o dar una palmada, el espectrograma se mueve y los LEDs reaccionan.
  • [ ] Si noto resets o parpadeos, he revisado alimentación, GND común y la resistencia serie en datos.

Con este caso práctico, has construido un pipeline completo i2s-spectrogram-websocket-neopixel sobre el modelo ESP32-DevKitC V4 + INMP441 I2S mic + WS2812B LED ring, asegurando coherencia en materiales, conexión, código y comandos de despliegue mediante PlatformIO y un conjunto concreto de versiones.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es la versión de Python requerida para este proyecto?




Pregunta 2: ¿Qué herramienta se menciona para la instalación del CLI de PlatformIO?




Pregunta 3: ¿Qué versión de PlatformIO Core se menciona en el artículo?




Pregunta 4: ¿Cuál es la plataforma específica de ESP32 mencionada en el artículo?




Pregunta 5: ¿Qué compilador se utiliza en la toolchain para ESP32?




Pregunta 6: ¿Cuál es la versión de la librería arduinoFFT mencionada?




Pregunta 7: ¿Qué driver se debe instalar en Windows para la placa ESP32-DevKitC V4?




Pregunta 8: ¿Cuál es el puerto serie típico en macOS para la placa ESP32?




Pregunta 9: ¿Qué tipo de cable se necesita para conectar el ESP32?




Pregunta 10: ¿Cuál es la versión de la librería NeoPixelBus mencionada?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:


Caso práctico: Detección de anomalías con I2S+MQTT en ESP32

Caso práctico: Detección de anomalías con I2S+MQTT en ESP32 — hero

Objetivo y caso de uso

Qué construirás: Un sistema de detección de anomalías utilizando I2S en un ESP32 que envía alertas a través de MQTT y proporciona retroalimentación de audio con indicadores LED.

Para qué sirve

  • Monitoreo de calidad de audio en tiempo real mediante análisis de señal.
  • Detección de ruidos anómalos en sistemas de audio profesional.
  • Alertas automáticas en dispositivos IoT cuando se detectan anomalías.
  • Control de iluminación LED en respuesta a eventos de audio específicos.
  • Integración con sistemas de domótica para mejorar la seguridad del hogar.

Resultado esperado

  • Latencias de respuesta de menos de 100 ms en la detección de anomalías.
  • Transmisión de datos a través de MQTT con un throughput de al menos 10 paquetes/s.
  • Indicadores LED que reflejan el estado del sistema en tiempo real.
  • Mensajes de alerta enviados al broker MQTT en menos de 200 ms tras la detección.
  • Capacidad de procesar señales de audio de hasta 48 kHz sin pérdida de calidad.

Público objetivo: Desarrolladores de sistemas embebidos; Nivel: Avanzado

Arquitectura/flujo: Captura de audio con I2S en ESP32 → Análisis de señal → Detección de anomalías → Envío de alertas MQTT → Activación de indicadores LED.

Nivel: Avanzado

Prerrequisitos

Sistemas operativos probados

  • Windows 11 Pro 23H2 (build 22631)
  • Ubuntu 22.04.4 LTS (kernel 5.15)
  • macOS 14.5 (Sonoma)

Toolchain exacta (versiones cerradas y reproducibles)

  • Python: 3.11.9
  • PlatformIO Core (CLI): 6.1.14
  • PlatformIO Platform: espressif32@6.5.0
  • Framework Arduino para ESP32 (a través de PlatformIO): 2.0.14
  • Compilador xtensa-esp32-elf-gcc: 8.4.0 (incluido por espressif32@6.5.0)
  • esptool.py: 4.7
  • Controlador USB-UART para Espressif ESP32-DevKitC V4: CP210x (Silicon Labs) v10.1.10 (Windows). En macOS y Linux generalmente no se requiere instalación manual.
  • Broker MQTT de referencia para validación: Eclipse Mosquitto 2.0.18 (opcional, para pruebas en PC)
  • Bibliotecas del proyecto (se fijan en platformio.ini):
  • PubSubClient 2.8 (MQTT)
  • Adafruit NeoPixel 1.12.0
  • arduinoFFT 1.5.6

Notas:
– Se fuerza el entorno a versiones concretas para favorecer la reproducibilidad.
– El framework Arduino 2.0.14 usa ESP-IDF 4.4.x bajo el capó, con API driver/i2s estable.

Materiales

  • Placa de desarrollo: Espressif ESP32-DevKitC V4 (ESP32-WROOM-32)
  • Micrófono digital I2S: INMP441 (módulo con pines WS/LRCL, SCK/BCLK, SD/DOUT, L/R)
  • Amplificador/DAC I2S: MAX98357A (módulo con BCLK, LRC, DIN, SD/GAIN)
  • Tira LED o LED suelto WS2812B (al menos 1 LED)
  • Fuente de alimentación:
  • USB 5 V para la DevKitC V4
  • 5 V para el WS2812B (puede tomarse del mismo USB si la corriente total lo permite)
  • Protoboard y cables dupont
  • Un altavoz pequeño (4–8 Ω) para el MAX98357A
  • Opcional pero recomendado: conversor de nivel lógico 3.3 V→5 V para la data del WS2812B (muchos funcionan sin él a 3.3 V, pero no es lo ideal)

Objetivo del proyecto: i2s-anomaly-detection-mqtt
– Captura de audio con INMP441 vía I2S0
– Generación de tonos con MAX98357A vía I2S1 para feedback acústico
– Detección de anomalías acústicas (basada en características espectrales) en tiempo real
– Telemetría MQTT con puntajes y eventos de anomalía
– Señalización visual con WS2812B (estado/calibración/anomalía)

Preparación y conexión

Consideraciones de pines y buses

  • El ESP32 dispone de dos controladores I2S (I2S_NUM_0 e I2S_NUM_1). Usaremos:
  • I2S0 para el micrófono (RX)
  • I2S1 para el amplificador MAX98357A (TX)
  • Evitaremos pines de “strapping” en señales sensibles de arranque (GPIO0, GPIO2, GPIO15) para minimizar riesgos de boot fallido.

Tabla de conexiones

Componente Señal/Pin módulo Pin ESP32-DevKitC V4 Notas
INMP441 VDD 3V3 Alimentación 3.3 V
INMP441 GND GND Tierra común
INMP441 L/R GND Salida en canal IZQUIERDO
INMP441 SCK (BCLK) GPIO14 I2S0 BCLK
INMP441 WS (LRCL) GPIO13 I2S0 LRCLK
INMP441 SD (DOUT) GPIO32 I2S0 DATA IN
MAX98357A VIN 5V Alimentación 5 V
MAX98357A GND GND Tierra común
MAX98357A BCLK GPIO26 I2S1 BCLK
MAX98357A LRC GPIO25 I2S1 LRCLK
MAX98357A DIN GPIO22 I2S1 DATA OUT
MAX98357A SD (EN/Mute) GPIO27 Control de mute/enable (alto = activo)
WS2812B +5V 5V Alimentación LED
WS2812B GND GND Tierra común
WS2812B DIN GPIO4 Datos LED (RMT/bitbang)

Recomendaciones:
– Conecta todas las tierras en común (ESP32, micrófono, MAX98357A, WS2812B).
– Si tu WS2812B es una tira larga, inyecta 5 V por ambos extremos y añade un condensador electrolítico 1000 µF/6.3 V entre +5 V y GND.
– Coloca una resistencia serie de 220–470 Ω en la línea DIN del WS2812B para mejorar integridad de señal.
– Si el cable entre ESP32 y LED es largo, usa conversor de nivel lógico 3.3→5 V.

Código completo (Arduino + PlatformIO)

A continuación se provee un proyecto mínimo viable completo con:
– Inicialización I2S RX (INMP441) e I2S TX (MAX98357A)
– Extracción de características (RMS y centroide espectral con FFT)
– Calibración de referencia (baseline) durante ~10 s
– Cálculo de puntaje de anomalía (z-score combinado)
– Publicación por MQTT en JSON
– Señalización con WS2812B y beep acústico en anomalías

Estructura propuesta del proyecto (PlatformIO):
– platformio.ini
– src/main.cpp

platformio.ini

; PlatformIO configuration for Espressif ESP32-DevKitC V4 (ESP32-WROOM-32)
[env:esp32dev]
platform = espressif32@6.5.0
board = esp32dev
framework = arduino
platform_packages =
  framework-arduinoespressif32@~2.0.14
monitor_speed = 115200
upload_speed = 921600
board_build.flash_mode = dio
board_build.f_cpu = 240000000L

lib_deps =
  knolleary/PubSubClient@2.8
  adafruit/Adafruit NeoPixel@1.12.0
  kosme/arduinoFFT@1.5.6

build_flags =
  -DWIFI_SSID=\"TU_SSID\"
  -DWIFI_PASS=\"TU_PASSWORD\"
  -DMQTT_HOST=\"192.168.1.50\"
  -DMQTT_PORT=1883
  -DMQTT_CLIENT_ID=\"esp32-devkitc-v4-i2s-anomaly\"
  -DMQTT_TOPIC_PUB=\"esp32/devkitc/anomaly\"
  -DMQTT_TOPIC_STATUS=\"esp32/devkitc/status\"
  -DAUDIO_SAMPLE_RATE=16000
  -DAUDIO_FRAME_SAMPLES=1024
  -DWS2812_PIN=4
  -DWS2812_NUM=1
  -DMAX98357_ENABLE_PIN=27

; Notas de toolchain:
; PlatformIO Core 6.1.14, esptool.py 4.7, xtensa-esp32-elf-gcc 8.4.0 (vía espressif32@6.5.0)

Asegúrate de reemplazar TU_SSID, TU_PASSWORD y MQTT_HOST por tus valores reales.

src/main.cpp

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_NeoPixel.h>
#include "arduinoFFT.h"
#include "driver/i2s.h"

// ======================= Configuración por macros (desde platformio.ini) =======================
#ifndef WIFI_SSID
#define WIFI_SSID "RELLENAR"
#endif
#ifndef WIFI_PASS
#define WIFI_PASS "RELLENAR"
#endif
#ifndef MQTT_HOST
#define MQTT_HOST "192.168.1.50"
#endif
#ifndef MQTT_PORT
#define MQTT_PORT 1883
#endif
#ifndef MQTT_CLIENT_ID
#define MQTT_CLIENT_ID "esp32-devkitc-v4-i2s-anomaly"
#endif
#ifndef MQTT_TOPIC_PUB
#define MQTT_TOPIC_PUB "esp32/devkitc/anomaly"
#endif
#ifndef MQTT_TOPIC_STATUS
#define MQTT_TOPIC_STATUS "esp32/devkitc/status"
#endif
#ifndef AUDIO_SAMPLE_RATE
#define AUDIO_SAMPLE_RATE 16000
#endif
#ifndef AUDIO_FRAME_SAMPLES
#define AUDIO_FRAME_SAMPLES 1024
#endif
#ifndef WS2812_PIN
#define WS2812_PIN 4
#endif
#ifndef WS2812_NUM
#define WS2812_NUM 1
#endif
#ifndef MAX98357_ENABLE_PIN
#define MAX98357_ENABLE_PIN 27
#endif

// ======================= Pines I2S (coherentes con la tabla) =======================
static const i2s_port_t I2S_MIC = I2S_NUM_0;
static const int I2S_MIC_BCLK = 14;   // INMP441 SCK/BCLK
static const int I2S_MIC_LRCL = 13;   // INMP441 WS/LRCL
static const int I2S_MIC_DOUT = 32;   // INMP441 SD/DOUT

static const i2s_port_t I2S_SPK = I2S_NUM_1;
static const int I2S_SPK_BCLK = 26;   // MAX98357A BCLK
static const int I2S_SPK_LRCL = 25;   // MAX98357A LRC
static const int I2S_SPK_DIN  = 22;   // MAX98357A DIN

// ======================= Objetos globales =======================
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
Adafruit_NeoPixel pixel(WS2812_NUM, WS2812_PIN, NEO_GRB + NEO_KHZ800);
arduinoFFT FFT = arduinoFFT(); // usaremos interfaz por vectores de doble precisión

// Buffers y variables de audio
static const uint16_t N = AUDIO_FRAME_SAMPLES; // tamaño de ventana FFT (potencia de 2)
int32_t i2s_rx_buffer[N];  // Lectura cruda 32-bit desde I2S
double vReal[N];
double vImag[N];

// Características y baseline
volatile bool baseline_ready = false;
const uint32_t baseline_time_ms = 10000; // 10 s de calibración
uint32_t baseline_start_ms = 0;
uint32_t frames_seen = 0;

double mean_rms = 0.0, var_rms = 0.0;
double mean_cent = 0.0, var_cent = 0.0;

const double anomaly_threshold = 6.0; // suma de |z-scores| de RMS y centroide

// Utilidades LED
void led_color(uint8_t r, uint8_t g, uint8_t b) {
  pixel.setPixelColor(0, pixel.Color(r, g, b));
  pixel.show();
}

// ======================= Inicialización I2S MIC (RX) =======================
bool i2s_mic_init() {
  i2s_config_t cfg = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = AUDIO_SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // INMP441 cableado a canal izquierdo
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 8,
    .dma_buf_len = 256,
    .use_apll = true,
    .tx_desc_auto_clear = false,
    .fixed_mclk = 0
  };
  i2s_pin_config_t pin_cfg = {
    .bck_io_num = I2S_MIC_BCLK,
    .ws_io_num = I2S_MIC_LRCL,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = I2S_MIC_DOUT
  };
  esp_err_t err;
  err = i2s_driver_install(I2S_MIC, &cfg, 0, NULL);
  if (err != ESP_OK) return false;
  err = i2s_set_pin(I2S_MIC, &pin_cfg);
  if (err != ESP_OK) return false;
  err = i2s_set_clk(I2S_MIC, AUDIO_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_32BIT, I2S_CHANNEL_MONO);
  return err == ESP_OK;
}

// ======================= Inicialización I2S SPK (TX) =======================
bool i2s_spk_init() {
  pinMode(MAX98357_ENABLE_PIN, OUTPUT);
  digitalWrite(MAX98357_ENABLE_PIN, LOW); // arranca en mute
  i2s_config_t cfg = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = AUDIO_SAMPLE_RATE, // mismo sample rate para simplicidad
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // el MAX98357A trabaja bien a 16 bits
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // estéreo (duplicaremos)
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 8,
    .dma_buf_len = 256,
    .use_apll = false,
    .tx_desc_auto_clear = true,
    .fixed_mclk = 0
  };
  i2s_pin_config_t pin_cfg = {
    .bck_io_num = I2S_SPK_BCLK,
    .ws_io_num = I2S_SPK_LRCL,
    .data_out_num = I2S_SPK_DIN,
    .data_in_num = I2S_PIN_NO_CHANGE
  };
  if (i2s_driver_install(I2S_SPK, &cfg, 0, NULL) != ESP_OK) return false;
  if (i2s_set_pin(I2S_SPK, &pin_cfg) != ESP_OK) return false;
  if (i2s_set_clk(I2S_SPK, AUDIO_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_STEREO) != ESP_OK) return false;
  digitalWrite(MAX98357_ENABLE_PIN, HIGH); // habilita amplificador
  return true;
}

// ======================= Generación de beep (feedback) =======================
void beep(uint16_t freq_hz = 880, uint16_t ms = 120, uint16_t amplitude = 4000) {
  const uint32_t samples = (AUDIO_SAMPLE_RATE * ms) / 1000;
  // Estéreo intercalado (L,R) a 16-bit
  for (uint32_t n = 0; n < samples; ++n) {
    float t = (float)n / AUDIO_SAMPLE_RATE;
    int16_t s = (int16_t)(amplitude * sinf(2.0f * PI * freq_hz * t));
    uint32_t twoch = ((uint16_t)s << 16) | ((uint16_t)s); // L=R
    size_t written = 0;
    i2s_write(I2S_SPK, &twoch, sizeof(twoch), &written, portMAX_DELAY);
  }
}

// ======================= Conectividad WiFi + MQTT =======================
void mqtt_reconnect() {
  while (!mqtt.connected()) {
    Serial.print(F("Conectando a MQTT... "));
    if (mqtt.connect(MQTT_CLIENT_ID)) {
      Serial.println(F("OK"));
      mqtt.publish(MQTT_TOPIC_STATUS, "online", true);
    } else {
      Serial.printf("fallo rc=%d, reintento en 3 s\n", mqtt.state());
      delay(3000);
    }
  }
}

void wifi_connect() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  Serial.printf("Conectando a WiFi SSID=%s\n", WIFI_SSID);
  uint32_t t0 = millis();
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);
    Serial.print(".");
    if (millis() - t0 > 20000) {
      Serial.println("\nTimeout WiFi, reintentando...");
      WiFi.disconnect(true);
      WiFi.begin(WIFI_SSID, WIFI_PASS);
      t0 = millis();
    }
  }
  Serial.printf("\nWiFi conectado: %s (RSSI=%d dBm), IP=%s\n",
                WiFi.SSID().c_str(), WiFi.RSSI(), WiFi.localIP().toString().c_str());
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
}

// ======================= Procesamiento de características =======================
static inline double compute_rms(const double* x, uint16_t len) {
  double acc = 0.0;
  for (uint16_t i = 0; i < len; ++i) acc += x[i] * x[i];
  return sqrt(acc / len);
}

static inline double compute_spectral_centroid(const double* mag, uint16_t bins, double fs) {
  // bins corresponde a N/2 después de FFT
  double num = 0.0, den = 0.0;
  for (uint16_t k = 1; k < bins; ++k) { // evita DC
    double f = (double)k * fs / (2.0 * bins);
    num += f * mag[k];
    den += mag[k];
  }
  return (den > 1e-9) ? (num / den) : 0.0;
}

// Welford para media/varianza en línea
struct OnlineStats {
  double mean = 0.0;
  double M2 = 0.0;
  uint32_t n = 0;
  void add(double x) {
    n++;
    double delta = x - mean;
    mean += delta / n;
    double delta2 = x - mean;
    M2 += delta * delta2;
  }
  double variance() const { return (n > 1) ? (M2 / (n - 1)) : 0.0; }
};

OnlineStats stats_rms, stats_cent;

// ======================= Setup =======================
void setup() {
  Serial.begin(115200);
  delay(200);

  pixel.begin();
  pixel.setBrightness(30); // brillo moderado
  led_color(255, 165, 0);  // naranja: arranque/calibración

  if (!i2s_mic_init()) {
    Serial.println("Error inicializando I2S MIC");
    led_color(255, 0, 255); // magenta error
    while (true) delay(1000);
  }
  if (!i2s_spk_init()) {
    Serial.println("Error inicializando I2S SPK");
    led_color(255, 0, 255);
    while (true) delay(1000);
  }

  wifi_connect();
  mqtt.setBufferSize(1024); // para payload JSON más amplio
  mqtt_reconnect();

  baseline_start_ms = millis();
  Serial.println("Calibración de baseline en curso (~10 s)...");
}

// ======================= Loop principal =======================
void loop() {
  if (WiFi.status() == WL_CONNECTED) {
    if (!mqtt.connected()) mqtt_reconnect();
    mqtt.loop();
  }

  // Leer un frame de AUDIO_FRAME_SAMPLES desde I2S (32-bit)
  size_t bytes_read = 0;
  i2s_read(I2S_MIC, (void*)i2s_rx_buffer, sizeof(i2s_rx_buffer), &bytes_read, portMAX_DELAY);
  uint16_t samples_read = bytes_read / sizeof(int32_t);
  if (samples_read != N) {
    // Ajuste básico; en la práctica debería llegar N por configuración de DMA
    return;
  }

  // Convertir a double, quitar offset y escalar
  // INMP441 a 32-bit, útil tomar los 24 bits superiores (datos suelen venir en MSB)
  static double dc_acc = 0.0;
  for (uint16_t i = 0; i < N; ++i) {
    int32_t s = i2s_rx_buffer[i] >> 8; // 24-bit significativo
    double x = (double)s / 8388608.0;  // normaliza aprox a [-1,1]
    dc_acc += 0.0001 * (x - dc_acc);   // filtro DC lento
    x -= dc_acc;
    // Ventana Hann para FFT
    double w = 0.5 * (1.0 - cos((2.0 * PI * i) / (N - 1)));
    vReal[i] = x * w;
    vImag[i] = 0.0;
  }

  // Característica temporal: RMS (sin ventana)
  double rms = compute_rms(vReal, N);

  // FFT -> magnitud espectral
  FFT.Windowing(vReal, N, FFT_WIN_TYP_RECTANGLE, FFT_FORWARD); // ya aplicamos Hann manual
  FFT.Compute(vReal, vImag, N, FFT_FORWARD);
  FFT.ComplexToMagnitude(vReal, vImag, N);

  // vReal ahora contiene magnitudes. Usar N/2 bins útiles.
  uint16_t bins = N / 2;
  double centroid = compute_spectral_centroid(vReal, bins, (double)AUDIO_SAMPLE_RATE);

  // Acumular baseline durante ~10 s
  if (!baseline_ready) {
    stats_rms.add(rms);
    stats_cent.add(centroid);
    frames_seen++;
    if (millis() - baseline_start_ms >= baseline_time_ms) {
      mean_rms = stats_rms.mean;
      var_rms  = stats_rms.variance();
      mean_cent = stats_cent.mean;
      var_cent  = stats_cent.variance();
      baseline_ready = true;
      Serial.printf("Baseline lista: mean_rms=%.6f, std_rms=%.6f, mean_cent=%.1f Hz, std_cent=%.1f Hz\n",
                    mean_rms, sqrt(var_rms), mean_cent, sqrt(var_cent));
      led_color(0, 255, 0); // verde: listo
      mqtt.publish(MQTT_TOPIC_STATUS, "baseline_ready", true);
    } else {
      // Indicar calibración con pulso suave
      static uint32_t lastBlink = 0;
      if (millis() - lastBlink > 250) {
        lastBlink = millis();
        led_color(255, 165, 0); // naranja
      }
    }
    return;
  }

  // Calcular z-scores y puntaje de anomalía
  double std_rms = sqrt(var_rms);
  double std_cent = sqrt(var_cent);
  double z_rms = (std_rms > 1e-9) ? (rms - mean_rms) / std_rms : 0.0;
  double z_cent = (std_cent > 1e-9) ? (centroid - mean_cent) / std_cent : 0.0;
  double anomaly_score = fabs(z_rms) + fabs(z_cent);
  bool is_anomaly = anomaly_score >= anomaly_threshold;

  // Feedback visual/acústico
  if (is_anomaly) {
    led_color(255, 0, 0);
    beep(880, 90, 3000);
  } else {
    led_color(0, 64, 0); // verde tenue en operación normal
  }

  // Publicar MQTT (JSON compacto)
  static char payload[768];
  snprintf(payload, sizeof(payload),
    "{\"device\":\"%s\",\"ts\":%lu,"
    "\"rms\":%.6f,\"centroid_hz\":%.1f,"
    "\"z_rms\":%.2f,\"z_cent\":%.2f,\"score\":%.2f,"
    "\"anomaly\":%s}",
    MQTT_CLIENT_ID, (unsigned long)millis(),
    rms, centroid,
    z_rms, z_cent, anomaly_score,
    is_anomaly ? "true" : "false"
  );
  mqtt.publish(MQTT_TOPIC_PUB, payload);
}

Breve explicación de partes clave:
– i2s_mic_init / i2s_spk_init: configura controladores I2S separados (RX para INMP441, TX para MAX98357A) con los pines exactos del apartado de conexiones.
– Lectura I2S: se leen 1024 muestras 32-bit, se normalizan y se aplica ventana Hann.
– Extracción de características:
– RMS: energía del frame.
– Centroide espectral: “centro de masa” del espectro, sensible a contenido de alta frecuencia.
– Baseline/calibración: durante 10 s calcula media y varianza (Welford) de RMS y centroide.
– Detección: suma de |z-scores|; si supera 6.0, se marca anomalía.
– Feedback:
– WS2812B: naranja (calibración), verde (normal), rojo (anomalía).
– MAX98357A: beep de ~880 Hz en anomalías.
– MQTT: publica JSON en esp32/devkitc/anomaly con métricas y bandera de anomalía.

Compilación/flash/ejecución

1) Instalar PlatformIO Core (CLI) en Python 3.11.9

  • Windows/macOS/Linux:
  • Asegúrate de tener Python 3.11 en PATH.
  • Instala versión fija:
    python -m pip install --upgrade pip
    python -m pip install platformio==6.1.14
    pio --version

    Debe imprimir “PlatformIO Core, version 6.1.14”.

2) Inicializar proyecto

  • En una carpeta vacía:
    pio project init --board esp32dev
  • Sustituye el contenido de platformio.ini por el mostrado arriba.
  • Crea src/main.cpp con el código anterior.

3) Drivers USB-UART

  • Windows: instala “CP210x Universal Windows Driver v10.1.10” desde Silicon Labs.
  • macOS/Linux: normalmente no se requiere. Verifica el puerto:
  • macOS: /dev/cu.SLAB_USBtoUART
  • Linux: /dev/ttyUSB0 o /dev/ttyUSB1
  • Windows: COMx

4) Comandos de build, upload y monitor

  • Compilar:
    pio run
  • Flashear (conecta la placa por USB):
    pio run -t upload
  • Monitor serie (115200 baudios):
    pio device monitor -b 115200

5) Comprobación del broker MQTT (opcional local)

  • Instala Mosquitto 2.0.18 (si no tienes broker).
  • Suscríbete para ver telemetría:
    mosquitto_sub -h 127.0.0.1 -p 1883 -t esp32/devkitc/anomaly -v
    mosquitto_sub -h 127.0.0.1 -p 1883 -t esp32/devkitc/status -v

Asegúrate de que MQTT_HOST en platformio.ini apunte a tu broker.

Validación paso a paso

1) Arranque y calibración
– Abre el monitor serie:
– Debes ver: “Conectando a WiFi…”, RSSI e IP asignada.
– Mensaje “Calibración de baseline en curso (~10 s)…”.
– LED WS2812B:
– Naranja durante la calibración (~10 s).
– MQTT:
– Topic esp32/devkitc/status: “online” y luego “baseline_ready”.

2) Post-calibración (estado normal)
– LED: verde tenue estable (normal).
– En el monitor serie se mostrará el resumen de baseline (mean/std de RMS y centroid).
– En el broker, llegarán mensajes JSON en esp32/devkitc/anomaly cada frame o cada pocos cientos de ms:
– Claves: rms, centroid_hz, z_rms, z_cent, score, anomaly.

3) Forzar una anomalía acústica
– Golpea suavemente la mesa, aplaude cerca o genera un ruido agudo.
– LED: pasa a rojo durante la detección del evento.
– Altavoz: debe sonar un beep breve (~880 Hz).
– MQTT: el JSON debe contener «anomaly»:true y un “score” significativamente > 6.0.

4) Verificación de parámetros
– Observa en JSON cómo varía “centroid_hz” (ruidos agudos subirán el centroide).
– “rms” aumentará con niveles de sonido más altos.
– “z_rms” y “z_cent” deberían acercarse a 0 en condiciones normales; crecerán en anomalías.

5) Suscripción desde PC con mosquitto_sub
– Ejemplo de salida:
esp32/devkitc/anomaly {"device":"esp32-devkitc-v4-i2s-anomaly","ts":10456,"rms":0.012345,"centroid_hz":1450.2,"z_rms":0.22,"z_cent":0.18,"score":0.40,"anomaly":false}
esp32/devkitc/anomaly {"device":"esp32-devkitc-v4-i2s-anomaly","ts":12890,"rms":0.098765,"centroid_hz":3800.5,"z_rms":4.10,"z_cent":3.20,"score":7.30,"anomaly":true}

6) Audio de salida
– Al detectar anomalía, el MAX98357A reproduce un beep corto; si no se escucha, revisa altavoz, cableado y nivel de mute (GPIO27 en alto).

Troubleshooting (errores típicos y soluciones)

1) El ESP32 no aparece como puerto serie o upload falla
– Verifica el cable USB (que sea de datos, no solo carga).
– Instala/actualiza el driver CP210x (Windows).
– Prueba a bajar la velocidad de upload: en platformio.ini usa upload_speed = 460800.
– Pulsa BOOT mientras inicia el upload, o reset si fuera necesario.

2) Boot loop o la placa no arranca tras cableado
– Evita usar pines de strapping (GPIO0, GPIO2, GPIO15) para señales externas. La guía ya los evita.
– Desconecta temporalmente periféricos y prueba solo la placa; si arranca, revisa resistencias/pulls en tu montaje.

3) El micrófono INMP441 no devuelve datos válidos (silencio o valores erráticos)
– Comprueba que L/R esté a GND para canal izquierdo.
– Revisa pines: SD→GPIO32, WS→GPIO13, SCK→GPIO14, VDD→3.3 V.
– Asegura tierra común.
– Mantén sample_rate=16000 y 32 bits en RX; INMP441 suele requerir 24 bits; usamos 32 con desplazamiento.

4) El MAX98357A no emite audio
– Verifica que SD/EN (GPIO27) esté en ALTO tras inicialización.
– Revisa BCLK (GPIO26), LRC (GPIO25), DIN (GPIO22).
– Comprueba el altavoz y su conexión, y que la fuente 5 V esté presente.
– Asegura la coincidencia de sample rate entre TX y la señal generada (usamos AUDIO_SAMPLE_RATE).

5) WS2812B no enciende o parpadea errático
– Añade resistencia serie de 330 Ω en DIN y condensador de 1000 µF en la alimentación de la tira.
– Usa un buen GND común y alimenta a 5 V estable.
– Si el cable DIN es largo, usa conversor de nivel 3.3→5 V.
– Disminuye el brillo (pixel.setBrightness).

6) No conecta a MQTT o se desconecta frecuentemente
– Verifica MQTT_HOST y puerto 1883 (sin TLS en este ejemplo).
– Asegúrate de que el broker permite clientes anónimos o configura usuario/clave en PubSubClient (no cubierto aquí).
– Aumenta keepalive o reduce frecuencia de publicaciones si el broker aplica rate limiting.

7) Anomalías constantes o nunca detecta
– Recalibra baseline en un entorno silencioso (~10 s tras reset).
– Ajusta anomaly_threshold (p. ej., 5.0 para mayor sensibilidad o 8.0 para menos).
– Valida que centroid_hz y rms varíen con estímulos; si no, revisa micrófono.

8) Desbordes/tiempos en MQTT (payload truncado)
– Aumenta mqtt.setBufferSize(1024) como en el código.
– Verifica que la longitud del JSON no exceda el buffer.

Mejoras/variantes

  • Modelo TinyML:
  • Integrar TensorFlow Lite for Microcontrollers para un autoencoder o detector de anomalías espectral. Preprocesa con mel-espectrogramas y despliega un modelo 1D.
  • Persistencia de baseline:
  • Guardar mean/std en NVS/Preferences tras calibración y restaurarlos en el arranque para evitar recalibrar cada vez.
  • MQTT seguro:
  • Migrar a TLS (puerto 8883) con certificados CA. Usar WiFiClientSecure y PubSubClient con setServer(MQTT_HOST, 8883).
  • Downsampling/banda de interés:
  • Filtrar banda (p. ej., 300–3400 Hz) para voz/máquinas y refinar el centroide o usar centroides por bandas.
  • Indicador multicolor más rico:
  • WS2812B en patrones animados (pulso, barrido) según severidad del score.
  • Buffer circular y detección por ráfagas:
  • Aplicar ventana deslizante, agregación de scores y supresión de múltiples eventos por debouncing temporal.
  • Telemetría ampliada:
  • Publicar espectro reducido (p. ej., 32 bins logarítmicos) o features adicionales: ZCR, entropía espectral, roll-off.

Checklist de verificación

  • [ ] He instalado PlatformIO Core 6.1.14 en Python 3.11.9 y confirmé pio –version.
  • [ ] He creado el proyecto con board=esp32dev y sustituido platformio.ini por el proporcionado.
  • [ ] He fijado WIFI_SSID, WIFI_PASS y MQTT_HOST en build_flags de platformio.ini.
  • [ ] He cableado el INMP441 a GPIO14/13/32 con L/R a GND y alimentación 3.3 V.
  • [ ] He cableado el MAX98357A a GPIO26/25/22 y EN en GPIO27, con alimentación 5 V y altavoz conectado.
  • [ ] He cableado el WS2812B a GPIO4 con 5 V y GND comunes, preferiblemente con resistencia en DIN.
  • [ ] La compilación pio run finaliza sin errores.
  • [ ] La carga pio run -t upload se realiza y puedo abrir pio device monitor -b 115200.
  • [ ] Veo “baseline_ready” tras ~10 s y LED verde.
  • [ ] Al provocar un ruido anómalo, LED rojo + beep, y el topic esp32/devkitc/anomaly publica anomaly=true.
  • [ ] En estado normal, el score permanece estable y bajo el umbral.

Con este caso práctico, has implementado un pipeline completo de i2s-anomaly-detection-mqtt en el hardware exacto “Espressif ESP32-DevKitC V4 (ESP32-WROOM-32) + INMP441 + MAX98357A + WS2812B”, con toolchain y versiones fijadas, incluyendo captura I2S, procesamiento básico de audio, notificación MQTT y feedback multimodal.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es el sistema operativo probado para el proyecto?




Pregunta 2: ¿Qué versión de Python se utiliza en la toolchain?




Pregunta 3: ¿Qué biblioteca se utiliza para MQTT en el proyecto?




Pregunta 4: ¿Cuál es el compilador utilizado en la toolchain?




Pregunta 5: ¿Qué tipo de micrófono se utiliza en el proyecto?




Pregunta 6: ¿Qué modelo de placa de desarrollo se menciona?




Pregunta 7: ¿Qué amplificador se utiliza en el proyecto?




Pregunta 8: ¿Cuál es el objetivo del proyecto mencionado?




Pregunta 9: ¿Qué fuente de alimentación se sugiere para la DevKitC V4?




Pregunta 10: ¿Qué versión de Eclipse Mosquitto se menciona como opcional?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:


Caso práctico: Gateway TWAI/CAN↔MQTT con ESP32, SN65HVD230

Caso práctico: Gateway TWAI/CAN↔MQTT con ESP32, SN65HVD230 — hero

Objetivo y caso de uso

Qué construirás: Un gateway que conecta redes TWAI/CAN con MQTT utilizando un ESP32-WROOM-32 y un transceptor SN65HVD230.

Para qué sirve

  • Integración de vehículos con sistemas IoT mediante la transmisión de datos de sensores a través de MQTT.
  • Monitoreo en tiempo real de parámetros de vehículos como velocidad y temperatura a través de una interfaz web.
  • Control de dispositivos remotos en el vehículo mediante comandos enviados a través de MQTT.
  • Visualización de datos en tiempo real en pantallas OLED utilizando el ESP32.

Resultado esperado

  • Latencia de comunicación menor a 100 ms entre el sensor y el broker MQTT.
  • Capacidad de manejar hasta 10 paquetes/s de datos desde el bus CAN.
  • Mensajes de estado de conexión MQTT con un 99% de disponibilidad.
  • Visualización de datos en la pantalla OLED con actualización cada 2 segundos.

Público objetivo: Usuarios avanzados; Nivel: Avanzado

Arquitectura/flujo: Comunicación entre el bus TWAI/CAN y el broker MQTT a través del ESP32, con visualización en OLED.

Nivel: Avanzado

Prerrequisitos

  • Sistemas operativos soportados (elige uno):
  • Windows 10/11 (64-bit) con derechos de administrador
  • macOS 13 Ventura / macOS 14 Sonoma (Apple Silicon o Intel)
  • Ubuntu 22.04 LTS / Debian 12 (amd64 o arm64)
  • Toolchain exacta (versionado cerrado y reproducible):
  • Python 3.10.12 (Ubuntu 22.04) o Python 3.11.x en macOS/Windows
  • PlatformIO Core 6.1.12 (CLI)
  • Plataforma ESP32 para PlatformIO: espressif32@6.5.0
  • Framework Arduino-ESP32 para PlatformIO: framework-arduinoespressif32@3.20014.0 (equivale a Arduino-ESP32 2.0.14)
  • Esptool.py 4.5.1 (a través de tool-esptoolpy@1.40501.0 en PlatformIO)
  • Librerías de usuario (PlatformIO lib_deps exactas):
    • PubSubClient@2.8 (MQTT)
    • Adafruit SSD1306@2.5.7 (OLED)
    • Adafruit GFX Library@1.11.9 (OLED)
    • ArduinoJson@6.21.5 (parseo/serialización de JSON)
  • Broker MQTT:
  • Eclipse Mosquitto 2.0.18 (cliente y/o servidor local)
  • Drivers USB-UART:
  • CP210x USB to UART Bridge VCP Drivers (para la mayoría de ESP32-WROOM-32 DevKitC, chip CP2102N/CP2102)
    • Windows: Versión 10.1.10 o superior
    • macOS/Linux: normalmente no requiere instalación manual

Verificaciones rápidas:
– USB: el puerto serie aparece al conectar el “ESP32-WROOM-32 DevKitC” (ej. COMx en Windows, /dev/tty.SLAB_USBtoUART en macOS, /dev/ttyUSBx en Linux).
– PlatformIO Core: pio –version debe mostrar 6.1.12.
– Mosquitto: mosquitto -h y mosquitto_sub -h no deben dar error.

Materiales

  • ESP32-WROOM-32 DevKitC + SN65HVD230 CAN Transceiver + SSD1306 OLED (modelo exacto solicitado)
  • Cables dupont macho-hembra
  • Bus CAN funcional (al menos otro nodo CAN a 500 kbit/s) y resistencia de terminación 120 Ω en los extremos (si tu transceptor o el otro nodo no la incorporan)
  • Fuente de alimentación por USB (5 V) para el DevKitC
  • Opcional: protoboard
  • Red Wi-Fi 2.4 GHz con acceso al broker MQTT (LAN local o remoto)

Notas de alimentación:
– SN65HVD230: alimentación a 3.3 V (no 5 V). El ESP32 entrega 3.3 V en su pin 3V3.
– SSD1306 I2C: alimentación a 3.3 V (recomendado); muchos módulos soportan 3–5 V, pero con ESP32 usa 3.3 V.

Preparación y conexión

Este proyecto configura un gateway “twai-can-mqtt-gateway”:
– Lado CAN (TWAI del ESP32) con transceptor SN65HVD230 a 500 kbit/s
– Lado MQTT sobre Wi-Fi hacia un broker (ej. Mosquitto)
– OLED SSD1306 muestra estado (Wi-Fi/MQTT, frames RX/TX, estado de bus)

Conexiones recomendadas (coherentes con código, pins y librerías):

Tabla de cableado

Elemento Pin/Señal en módulo Conectar a ESP32-WROOM-32 DevKitC Notas
SN65HVD230 VCC 3V3 3.3 V únicamente
SN65HVD230 GND GND Masa común
SN65HVD230 D (TXD) GPIO 5 (TWAI_TX) Salida MCU → entrada transceptor
SN65HVD230 R (RXD) GPIO 4 (TWAI_RX) Entrada MCU ← salida transceptor
SN65HVD230 CANH CANH del bus Par trenzado con CANL
SN65HVD230 CANL CANL del bus Par trenzado con CANH
SN65HVD230 RS (mode/slope) GND RS a GND = modo normal alta velocidad
SSD1306 OLED (I2C) VCC 3V3 Alimentación 3.3 V
SSD1306 OLED (I2C) GND GND Masa común
SSD1306 OLED (I2C) SDA GPIO 21 (I2C SDA) Bus I2C
SSD1306 OLED (I2C) SCL GPIO 22 (I2C SCL) Bus I2C

Observaciones:
– Asegura que la línea CAN esté terminada con 120 Ω en ambos extremos del bus. Muchas placas SN65HVD230 incluyen un jumper/solder bridge para activar una resistencia de 120 Ω local. Actívala solo si esta placa es un extremo del bus.
– Mantén separados los cables de CAN (CANH/CANL) del resto de señales para minimizar ruido.
– El RS del SN65HVD230 controla standby y control de flanco. A GND entra en modo de velocidad alta (lo recomendado para este caso).
– No mezcles niveles de 5 V en señales lógicas con el ESP32.

Código completo

A continuación se proveen los archivos clave del proyecto PlatformIO. El código implementa:
– Inicialización de TWAI (CAN) a 500 kbit/s, filtros “accept all”
– Suscripción MQTT al tópico “twai/gw/can/tx” para inyectar frames CAN desde JSON
– Publicación de frames recibidos en “twai/gw/can/rx” en formato JSON
– Estadísticas y estado en “twai/gw/status” y “twai/gw/stats”
– OLED SSD1306 con 4 líneas: estado Wi-Fi/MQTT, estado CAN, contadores y broker/ip

platformio.ini

Crea platformio.ini en la raíz del proyecto con el siguiente contenido:

[env:esp32dev]
platform = espressif32@6.5.0
board = esp32dev
framework = arduino
platform_packages =
  framework-arduinoespressif32@3.20014.0
  tool-esptoolpy@1.40501.0
lib_deps =
  knolleary/PubSubClient@2.8
  adafruit/Adafruit SSD1306@2.5.7
  adafruit/Adafruit GFX Library@1.11.9
  bblanchon/ArduinoJson@6.21.5
upload_speed = 921600
monitor_speed = 115200
monitor_filters = time, colorize
build_flags =
  -D CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL=4
  -D WIFI_SSID="\"TU_SSID\""
  -D WIFI_PASS="\"TU_PASSWORD\""
  -D MQTT_HOST="\"192.168.1.10\""
  -D MQTT_PORT=1883

Personaliza WIFI_SSID, WIFI_PASS y MQTT_HOST/MQTT_PORT a tu entorno.

src/main.cpp

Crea src/main.cpp con el contenido completo:

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <driver/twai.h>
#include <ArduinoJson.h>

// Pines y parámetros de hardware
#define TWAI_TX_GPIO GPIO_NUM_5   // SN65HVD230 D <- MCU TX
#define TWAI_RX_GPIO GPIO_NUM_4   // SN65HVD230 R -> MCU RX
#define I2C_SDA      21
#define I2C_SCL      22
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET   -1 // SSD1306 sin pin reset dedicado
#define OLED_ADDR    0x3C

// Configuración de red/MQTT (inyectadas por build_flags en platformio.ini)
#ifndef WIFI_SSID
#define WIFI_SSID "TU_SSID"
#endif
#ifndef WIFI_PASS
#define WIFI_PASS "TU_PASSWORD"
#endif
#ifndef MQTT_HOST
#define MQTT_HOST "192.168.1.10"
#endif
#ifndef MQTT_PORT
#define MQTT_PORT 1883
#endif

// Tópicos MQTT
static const char* TOPIC_RX     = "twai/gw/can/rx";
static const char* TOPIC_TX     = "twai/gw/can/tx";
static const char* TOPIC_STATUS = "twai/gw/status";
static const char* TOPIC_STATS  = "twai/gw/stats";
static const char* TOPIC_LOG    = "twai/gw/log";

WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Estado
volatile uint32_t can_rx_count = 0;
volatile uint32_t can_tx_count = 0;
volatile uint32_t can_rx_errors = 0;
volatile uint32_t can_tx_errors = 0;
volatile uint32_t can_bus_offs = 0;

String ipStr;
unsigned long lastStatsMs = 0;

static void oledPrintStatus(const String& line1, const String& line2, const String& line3, const String& line4) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println(line1);
  display.setCursor(0, 16);
  display.println(line2);
  display.setCursor(0, 32);
  display.println(line3);
  display.setCursor(0, 48);
  display.println(line4);
  display.display();
}

static void updateOLEDStatus() {
  twai_status_info_t status;
  twai_get_status_info(&status);

  char l1[32], l2[32], l3[32], l4[32];
  snprintf(l1, sizeof(l1), "WiFi:%s MQTT:%s",
           WiFi.isConnected() ? "OK" : "NO",
           mqtt.connected() ? "OK" : "NO");

  const char* st = "BUS_ON";
  if (status.state == TWAI_STATE_BUS_OFF) st = "BUS_OFF";
  else if (status.state == TWAI_STATE_ERROR_PASSIVE) st = "ERR_PASS";
  else if (status.state == TWAI_STATE_RECOVERING) st = "RECOV";

  snprintf(l2, sizeof(l2), "CAN:%s RXQ:%d TXQ:%d", st, status.msgs_to_rx, status.msgs_to_tx);
  snprintf(l3, sizeof(l3), "RX:%lu TX:%lu", (unsigned long)can_rx_count, (unsigned long)can_tx_count);
  snprintf(l4, sizeof(l4), "MQTT:%s %s", MQTT_HOST, ipStr.c_str());

  oledPrintStatus(l1, l2, l3, l4);
}

static void publishStatus() {
  StaticJsonDocument<256> doc;
  doc["wifi"]["connected"] = WiFi.isConnected();
  doc["wifi"]["ip"] = ipStr;
  doc["mqtt"]["connected"] = mqtt.connected();
  twai_status_info_t status;
  twai_get_status_info(&status);
  const char* st = "BUS_ON";
  if (status.state == TWAI_STATE_BUS_OFF) st = "BUS_OFF";
  else if (status.state == TWAI_STATE_ERROR_PASSIVE) st = "ERROR_PASSIVE";
  else if (status.state == TWAI_STATE_RECOVERING) st = "RECOVERING";
  doc["twai"]["state"] = st;
  doc["twai"]["rx_queue"] = status.msgs_to_rx;
  doc["twai"]["tx_queue"] = status.msgs_to_tx;
  doc["twai"]["rx_count"] = can_rx_count;
  doc["twai"]["tx_count"] = can_tx_count;
  doc["twai"]["rx_errs"] = can_rx_errors;
  doc["twai"]["tx_errs"] = can_tx_errors;
  doc["twai"]["bus_offs"] = can_bus_offs;

  char out[384];
  size_t n = serializeJson(doc, out, sizeof(out));
  mqtt.publish(TOPIC_STATUS, out, n);
}

static void publishStats() {
  StaticJsonDocument<192> doc;
  doc["rx"] = can_rx_count;
  doc["tx"] = can_tx_count;
  doc["host"] = MQTT_HOST;
  char out[192];
  size_t n = serializeJson(doc, out, sizeof(out));
  mqtt.publish(TOPIC_STATS, out, n);
}

static String hexBytes(const uint8_t* d, uint8_t len) {
  static const char* hex = "0123456789ABCDEF";
  String s;
  s.reserve(len * 2);
  for (uint8_t i = 0; i < len; i++) {
    s += hex[(d[i] >> 4) & 0xF];
    s += hex[d[i] & 0xF];
  }
  return s;
}

static bool parseHexData(const char* s, uint8_t* buf, uint8_t* out_len) {
  size_t L = strlen(s);
  if (L % 2 != 0) return false;
  size_t bytes = L / 2;
  if (bytes > 8) return false;
  for (size_t i = 0; i < bytes; i++) {
    char c1 = s[2 * i];
    char c2 = s[2 * i + 1];
    auto hv = [](char c) -> int {
      if (c >= '0' && c <= '9') return c - '0';
      if (c >= 'a' && c <= 'f') return c - 'a' + 10;
      if (c >= 'A' && c <= 'F') return c - 'A' + 10;
      return -1;
    };
    int h = hv(c1), l = hv(c2);
    if (h < 0 || l < 0) return false;
    buf[i] = (uint8_t)((h << 4) | l);
  }
  *out_len = (uint8_t)bytes;
  return true;
}

// Envía un frame CAN a partir de JSON recibido en TOPIC_TX
static void handleTxJson(const char* payload, size_t len) {
  StaticJsonDocument<256> doc;
  DeserializationError err = deserializeJson(doc, payload, len);
  if (err) {
    mqtt.publish(TOPIC_LOG, "JSON parse error on TX");
    return;
  }
  twai_message_t m = {};
  m.extd = doc["ext"] | false;
  m.rtr = doc["rtr"] | false;
  m.identifier = doc["id"] | 0;
  if (m.extd) {
    if (m.identifier > 0x1FFFFFFF) m.identifier = 0x1FFFFFFF;
  } else {
    if (m.identifier > 0x7FF) m.identifier = 0x7FF;
  }

  if (!m.rtr) {
    const char* dataHex = doc["data"] | "";
    uint8_t lenParsed = 0;
    uint8_t tmp[8] = {0};
    if (!parseHexData(dataHex, tmp, &lenParsed)) {
      mqtt.publish(TOPIC_LOG, "Invalid hex data");
      return;
    }
    m.data_length_code = doc["dlc"] | lenParsed;
    if (m.data_length_code > lenParsed) m.data_length_code = lenParsed;
    memcpy(m.data, tmp, m.data_length_code);
  } else {
    m.data_length_code = doc["dlc"] | 0;
  }

  esp_err_t res = twai_transmit(&m, pdMS_TO_TICKS(100));
  if (res == ESP_OK) {
    can_tx_count++;
    StaticJsonDocument<192> ack;
    ack["tx_ack"] = true;
    ack["id"] = m.identifier;
    ack["ext"] = (bool)m.extd;
    ack["dlc"] = m.data_length_code;
    char out[192];
    size_t n = serializeJson(ack, out, sizeof(out));
    mqtt.publish("twai/gw/can/tx_ack", out, n);
  } else {
    can_tx_errors++;
    mqtt.publish(TOPIC_LOG, "TX failed");
  }
}

static void mqttCallback(char* topic, byte* payload, unsigned int length) {
  if (strcmp(topic, TOPIC_TX) == 0) {
    handleTxJson((const char*)payload, (size_t)length);
  }
}

static void ensureWifi() {
  if (WiFi.isConnected()) return;
  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) {
    delay(250);
  }
  if (WiFi.isConnected()) {
    ipStr = WiFi.localIP().toString();
  } else {
    ipStr = "0.0.0.0";
  }
}

static void ensureMqtt() {
  if (mqtt.connected()) return;
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  String clientId = String("twai-gw-esp32-") + String((uint32_t)ESP.getEfuseMac(), HEX);
  if (mqtt.connect(clientId.c_str())) {
    mqtt.subscribe(TOPIC_TX);
    mqtt.publish(TOPIC_LOG, "MQTT connected");
  }
}

// Tarea de recepción CAN → MQTT
void can_rx_task(void* arg) {
  while (true) {
    twai_message_t m;
    esp_err_t res = twai_receive(&m, pdMS_TO_TICKS(100));
    if (res == ESP_OK) {
      can_rx_count++;
      StaticJsonDocument<256> doc;
      doc["id"] = m.identifier;
      doc["ext"] = (bool)m.extd;
      doc["rtr"] = (bool)m.rtr;
      doc["dlc"] = m.data_length_code;
      if (!m.rtr && m.data_length_code <= 8) {
        doc["data"] = hexBytes(m.data, m.data_length_code);
      }
      char out[256];
      size_t n = serializeJson(doc, out, sizeof(out));
      mqtt.publish(TOPIC_RX, out, n);
    } else if (res == ESP_ERR_TIMEOUT) {
      // Nada en cola; continúa
    } else if (res == ESP_ERR_INVALID_STATE) {
      // Driver detenido/bus_off; cuenta y espera
      can_bus_offs++;
      vTaskDelay(pdMS_TO_TICKS(100));
    } else {
      can_rx_errors++;
    }
  }
}

// Inicializa TWAI (CAN) a 500 kbit/s
static bool initTWAI() {
  twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TWAI_TX_GPIO, TWAI_RX_GPIO, TWAI_MODE_NORMAL);
  twai_timing_config_t  t_config = TWAI_TIMING_CONFIG_500KBITS();
  twai_filter_config_t  f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

  if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
    Serial.println("twai_driver_install failed");
    return false;
  }
  if (twai_start() != ESP_OK) {
    Serial.println("twai_start failed");
    return false;
  }
  // Opcional: fijar alertas (no indispensable en este caso)
  return true;
}

void setup() {
  Serial.begin(115200);
  delay(100);

  // I2C + OLED
  Wire.begin(I2C_SDA, I2C_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("twai-can-mqtt-gateway");
  display.println("Init...");
  display.display();

  // Wi-Fi
  ensureWifi();

  // MQTT
  mqtt.setCallback(mqttCallback);
  ensureMqtt();

  // TWAI
  if (!initTWAI()) {
    oledPrintStatus("TWAI init FAILED", "", "", "");
    while (true) { delay(1000); }
  }

  // Tarea de recepción CAN
  xTaskCreatePinnedToCore(can_rx_task, "can_rx_task", 4096, nullptr, 10, nullptr, 1);

  updateOLEDStatus();
  publishStatus();
}

void loop() {
  // Mantener Wi-Fi/MQTT
  if (!WiFi.isConnected()) {
    ensureWifi();
  }
  if (WiFi.isConnected() && !mqtt.connected()) {
    ensureMqtt();
  }
  mqtt.loop();

  // Estadísticas cada 1 s
  unsigned long now = millis();
  if (now - lastStatsMs > 1000) {
    lastStatsMs = now;
    updateOLEDStatus();
    if (mqtt.connected()) {
      publishStatus();
      publishStats();
    }
  }

  delay(10);
}

Explicación de partes clave:
– TWAI a 500 kbit/s: TWAI_GENERAL_CONFIG_DEFAULT con GPIO 5 (TX) y 4 (RX). Timing con TWAI_TIMING_CONFIG_500KBITS(), filtro de aceptación total.
– MQTT PubSubClient: publica frames RX en JSON y escucha en el tópico de TX para inyectar frames.
– ArduinoJson: garantiza parseo/serialización robusto y limitado en memoria (documentos estáticos).
– OLED: muestra estado en tiempo real (Wi-Fi/MQTT, estado del controlador TWAI, contadores, host del broker e IP).
– Tareas: can_rx_task en núcleo 1 evita bloquear el loop principal que se encarga de Wi-Fi/MQTT.

Formato de JSON para transmisión (tópico twai/gw/can/tx):
– id: entero (11 bits si ext=false, 29 bits si ext=true)
– ext: booleano (false por defecto)
– rtr: booleano (false por defecto)
– dlc: 0–8 (opcional; si no se provee, se infiere de la longitud de ‘data’)
– data: string hex (hasta 16 chars para 8 bytes; p.ej. «112233AABBCCDD00»)

Ejemplo:
{«id»: 291, «ext»: false, «rtr»: false, «dlc»: 8, «data»: «1122334455667788»}

Compilación/flash/ejecución

Asumiendo PlatformIO Core 6.1.12 instalado y en PATH.

1) Crear proyecto

  • Usando CLI:
# Linux/macOS
mkdir -p ~/projects/twai-can-mqtt-gateway
cd ~/projects/twai-can-mqtt-gateway
pio project init --board esp32dev --project-option "platform=espressif32@6.5.0" --project-option "framework=arduino"

# Windows PowerShell
mkdir $env:USERPROFILE\projects\twai-can-mqtt-gateway
cd $env:USERPROFILE\projects\twai-can-mqtt-gateway
pio project init --board esp32dev --project-option "platform=espressif32@6.5.0" --project-option "framework=arduino"
  • Sustituye el contenido de platformio.ini por el dado anteriormente.
  • Crea src/main.cpp con el código completo mostrado.

2) Instalar dependencias

PlatformIO las bajará en el primer build, pero puedes forzar:

pio pkg update
pio lib -e esp32dev install "knolleary/PubSubClient@2.8"
pio lib -e esp32dev install "adafruit/Adafruit SSD1306@2.5.7"
pio lib -e esp32dev install "adafruit/Adafruit GFX Library@1.11.9"
pio lib -e esp32dev install "bblanchon/ArduinoJson@6.21.5"

3) Compilar

pio run

4) Conectar el ESP32-WROOM-32 DevKitC por USB

  • Identifica el puerto (ejemplos):
  • Windows: COM5
  • macOS: /dev/tty.SLAB_USBtoUART
  • Linux: /dev/ttyUSB0

5) Flashear

pio run -t upload

Si necesitas especificar puerto:

pio run -t upload --upload-port /dev/ttyUSB0

6) Monitor serie

pio device monitor -b 115200

Deberías ver logs de conexión Wi-Fi, conexión MQTT y arranque de TWAI.

7) Broker MQTT (Mosquitto 2.0.18)

  • Ubuntu 22.04:
sudo apt-get update
sudo apt-get install -y mosquitto=2.0.18-0mosquitto1~jammy1 mosquitto-clients=2.0.18-0mosquitto1~jammy1
sudo systemctl enable --now mosquitto
  • Verificación:
mosquitto -h | head -n 3
mosquitto_sub -h 127.0.0.1 -t "#" -v
  • Alternativa con Docker:
docker run --rm -it -p 1883:1883 eclipse-mosquitto:2.0.18

Ajusta MQTT_HOST del proyecto (platformio.ini) a la IP del broker.

Validación paso a paso

1) Verifica alimentación y cableado:
– SN65HVD230: VCC a 3V3; GND a GND; RS a GND; D a GPIO 5; R a GPIO 4.
– SSD1306: VCC a 3V3; GND a GND; SDA a GPIO 21; SCL a GPIO 22.
– CANH/CANL conectados al bus y terminación 120 Ω presente en extremos.

2) Arranque:
– En el monitor serie deben aparecer mensajes como “MQTT connected” y “twai_start OK”.
– En la OLED:
– Línea 1: WiFi:OK MQTT:OK (tras conexión)
– Línea 2: CAN:BUS_ON RXQ:0 TXQ:0 (al inicio)
– Línea 3: RX:0 TX:0 (contadores)
– Línea 4: MQTT:192.168.1.10 192.168.1.xx (IP del ESP32)

3) Suscríbete a los tópicos desde tu PC:

mosquitto_sub -h 192.168.1.10 -t 'twai/gw/#' -v

4) Inyecta un frame desde MQTT hacia CAN (si tienes otro nodo escuchando en el bus):

mosquitto_pub -h 192.168.1.10 -t 'twai/gw/can/tx' -m '{"id":291,"ext":false,"rtr":false,"dlc":8,"data":"1122334455667788"}'
  • Esperado:
  • Publicación ACK en “twai/gw/can/tx_ack” con id=291 y dlc=8
  • OLED incrementa TX
  • El otro nodo CAN debe recibir el frame (si lo monitorizas con equipo externo o un segundo nodo ESP32)

5) Ingresos desde el bus a MQTT:
– Genera frames desde otro nodo CAN a 500 kbit/s y observa en:

mosquitto_sub -h 192.168.1.10 -t 'twai/gw/can/rx' -v
  • El payload JSON debe mostrar id, ext, rtr, dlc y data en hex.

6) Estado y estadísticas:

mosquitto_sub -h 192.168.1.10 -t 'twai/gw/status' -v
mosquitto_sub -h 192.168.1.10 -t 'twai/gw/stats' -v
  • Debe mostrar bus_on o estados de error si los hubiera, colas, contadores y dirección IP.

7) Robustez:
– Apaga el broker y vuelve a encenderlo: observa reconexión automática (OLED: MQTT:NO → OK).
– Desconecta el bus CAN o genera errores: observa cambios de estado (ERROR_PASSIVE, RECOVERING, BUS_OFF) en status.

Troubleshooting

1) No aparece puerto serie:
– Windows: instala/reinstala “CP210x VCP Drivers” (Silicon Labs), usa otro cable USB de datos, prueba otro puerto.
– macOS/Linux: revisa permisos del puerto (/dev/ttyUSBx o /dev/tty.SLAB_USBtoUART). En Linux, añade tu usuario al grupo dialout: sudo usermod -aG dialout $USER y reloguea.

2) Fallo de flasheo (Timed out waiting for packet header):
– Mantén pulsado BOOT mientras reseteas (EN) el ESP32 y suelta tras iniciar upload.
– Reduce upload_speed en platformio.ini a 460800 o 115200.

3) OLED sin imagen:
– Revisa dirección I2C (0x3C es común; algunos módulos usan 0x3D). Ajusta OLED_ADDR en el código si es necesario.
– Verifica SDA/SCL (GPIO 21/22) y alimentación a 3.3 V.
– Comprueba que display.begin(…) no falle (muestra mensaje en Serial si quieres depurar).

4) MQTT no conecta:
– Verifica MQTT_HOST y MQTT_PORT. Comprueba reachability: ping MQTT_HOST y telnet MQTT_HOST 1883 (o nmap).
– Si el broker requiere autenticación/TLS, ajusta PubSubClient (no cubierto aquí). Para broker local, desactiva listeners TLS.

5) No recibo frames en twai/gw/can/rx:
– Revisa bitrate del bus (500 kbit/s en ambos nodos).
– Asegura terminaciones 120 Ω.
– Cruza CANH/CANL correctamente (no inviertas polaridad).
– Confirma que SN65HVD230 está a 3.3 V y RS a GND (standby desactivado).

6) TX falla (TX failed) o contador de errores sube:
– La línea CAN puede estar desconectada o en BUS_OFF. Verifica estado en tópico twai/gw/status.
– Comprueba que exista al menos otro nodo activo en el bus (CAN requiere confirmación de ACK por otro nodo).
– Revisa longitud del cableado y calidad del par CAN.

7) Congestión MQTT o reinicios:
– Reduce tasa de frames o filtra por IDs (modifica filtro y/o publica con QoS 0).
– Asegura buena cobertura Wi-Fi y alimentación estable (USB 5 V robusto).

8) JSON inválido o data hex incorrecta:
– Asegúrate de pares hex válidos (0–9, A–F). Longitud máxima 16 caracteres para 8 bytes.
– Si usas RTR, normalmente dlc>0 y sin campo data.

Mejoras/variantes

  • Seguridad y robustez:
  • Añadir autenticación MQTT (usuario/contraseña) y reconexión con backoff exponencial.
  • TLS con certificados (usar WiFiClientSecure y broker TLS en 8883).
  • Filtrado y enrutado:
  • Implementar listas de IDs permitidos/denegados y mapeo de tópicos por ID (p.ej. twai/gw/can/rx/0x123).
  • Añadir soporte para CAN FD (no soportado por ESP32 clásico; requeriría hardware diferente).
  • Métricas y telemetría:
  • Publicar tasa de frames (frames/s), errores acumulados por tipo, estado de alertas TWAI.
  • Persistencia/config:
  • Guardar configuración Wi-Fi/MQTT/bitrate en NVS y exponer comandos por MQTT para cambiarlos en caliente.
  • Interfaz de usuario:
  • Páginas en OLED para ver últimas tramas, ID más frecuente, tiempo desde última RX/TX.
  • Integración:
  • Contenedor Docker para broker y dashboard (Node-RED + Mosquitto + InfluxDB + Grafana).
  • Diagnóstico:
  • Añadir tópico para reiniciar controlador TWAI en BUS_OFF y medir recuperación.

Checklist de verificación

  • [ ] He instalado PlatformIO Core 6.1.12 y verifico pio –version
  • [ ] He configurado platformio.ini con:
  • [ ] platform espressif32@6.5.0
  • [ ] framework-arduinoespressif32@3.20014.0
  • [ ] tool-esptoolpy@1.40501.0
  • [ ] lib_deps con versiones: PubSubClient@2.8, Adafruit SSD1306@2.5.7, Adafruit GFX@1.11.9, ArduinoJson@6.21.5
  • [ ] He cableado exactamente: SN65HVD230 D→GPIO5, R→GPIO4, RS→GND, VCC→3V3; SSD1306 SDA→GPIO21, SCL→GPIO22, VCC→3V3
  • [ ] He verificado terminación 120 Ω en los extremos del bus CAN
  • [ ] He ajustado WIFI_SSID, WIFI_PASS, MQTT_HOST, MQTT_PORT en platformio.ini
  • [ ] El proyecto compila con pio run sin errores
  • [ ] Se programa con pio run -t upload y se abre monitor con pio device monitor -b 115200
  • [ ] En OLED veo WiFi:OK y MQTT:OK y estado CAN:BUS_ON
  • [ ] Puedo suscribirme a twai/gw/can/rx y ver frames al inyectar desde otro nodo
  • [ ] Puedo publicar en twai/gw/can/tx con JSON válido y ver ACK y aumento del contador TX
  • [ ] En caso de problemas, he revisado la sección Troubleshooting

Con esta guía avanzada has montado un gateway “twai-can-mqtt-gateway” totalmente reproducible sobre el hardware exacto ESP32-WROOM-32 DevKitC + SN65HVD230 CAN Transceiver + SSD1306 OLED, utilizando una toolchain definida y versiones concretas de framework y librerías, con rutas, comandos y validación paso a paso.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es el sistema operativo soportado que requiere derechos de administrador?




Pregunta 2: ¿Qué versión de Python se requiere para Ubuntu 22.04?




Pregunta 3: ¿Cuál es la versión de PlatformIO Core necesaria?




Pregunta 4: ¿Qué librería se utiliza para el parseo y serialización de JSON?




Pregunta 5: ¿Qué broker MQTT se menciona en el artículo?




Pregunta 6: ¿Cuál es la versión mínima de los drivers CP210x para Windows?




Pregunta 7: ¿Qué tipo de cables se mencionan como parte de los materiales?




Pregunta 8: ¿Qué tipo de transceptor se menciona en los materiales?




Pregunta 9: ¿Qué resistencia se requiere en el bus CAN?




Pregunta 10: ¿Qué comando se debe usar para verificar la versión de PlatformIO Core?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:


Caso práctico: Vibración predictiva LoRa con ESP32-S3

Caso práctico: Vibración predictiva LoRa con ESP32-S3 — hero

Objetivo y caso de uso

Qué construirás: Un sistema de detección de vibraciones predictivo utilizando LoRa y ESP32-S3 para monitorizar maquinaria industrial.

Para qué sirve

  • Monitoreo continuo de vibraciones en motores eléctricos para prevenir fallos.
  • Detección temprana de desbalance en ejes rotativos en fábricas.
  • Transmisión de datos de vibración en tiempo real a través de LoRa para análisis remoto.
  • Integración con sistemas de gestión de mantenimiento para optimizar intervenciones.

Resultado esperado

  • Reducción del 30% en tiempos de inactividad no planificada.
  • Latencia de transmisión de datos menor a 5 segundos.
  • Capacidad de enviar hasta 100 paquetes de datos por hora.
  • Precisión en la medición de vibraciones de ±0.1 g.

Público objetivo: Ingenieros de mantenimiento; Nivel: alto

Arquitectura/flujo: Sensores ICM-42688-P conectados al ESP32-S3, que envían datos a través de LoRa a un servidor para análisis.

Nivel: alto

Prerrequisitos

  • Sistema operativo (elige uno; todos probados con las versiones indicadas):
  • Windows 11 23H2 (Build 22631)
  • Ubuntu 22.04.4 LTS
  • macOS 14 Sonoma

  • Toolchain exacta:

  • Python 3.10.x (Ubuntu/macOS) o Python 3.11.x (Windows, vía Microsoft Store). Verifica con: python –version
  • PlatformIO Core 6.1.15 (CLI) y PlatformIO IDE 3.1.1 (VS Code 1.93+)
  • Verifica: pio –version → PlatformIO Core, Home y Python
  • Plataforma ESP32 para PlatformIO:
  • platform = espressif32 @ 6.6.0
  • framework-arduinoespressif32 @ 3.0.7 (Arduino-ESP32 basado en ESP-IDF 5.x)
  • toolchain-xtensa-esp32s3 @ 12.2.0+2023r1 (xtensa-esp32s3-elf-gcc 12.2.0)
  • Librerías Arduino (fijadas para reproducibilidad):
  • RadioLib @ 6.5.0 (manejo SX1276/RFM95W)
  • hideakitai/ICM42688 @ 0.2.2 (ICM-42688-P por I2C)
  • arduinoFFT @ 2.0.3 (FFT en punto flotante)
  • cryptoauthlib @ 3.3.3 (Microchip ATECC608A)
  • Controladores USB:
  • ESP32-S3-DevKitC-1 usa USB nativo CDC/JTAG; en Windows y macOS, no se requieren drivers adicionales.
  • Si usas un adaptador USB-UART alternativo: CP210x v10.1.12 (Windows) / v6.0.1 (macOS), o CH34x v1.7 (Windows/macOS).

Notas:
– La ESP32-S3-DevKitC-1 suele programarse por USB nativo (Type-C). Asegúrate de que el “USB CDC On Boot” está activo (lo fijaremos desde PlatformIO).
– En Linux, añade tu usuario a dialout para acceder a /dev/ttyACM*: sudo usermod -a -G dialout $USER y relogin.

Materiales

  • Conjunto exacto:
  • 1 × ESP32-S3-DevKitC-1 (ESP32-S3)
  • 1 × RFM95W (SX1276) 868/915 MHz (elige la variante para tu región, p. ej. 868 MHz para EU868)
  • 1 × ICM-42688-P (IMU 6 DoF; breakout típico: GND/VCC/SDA/SCL/AD0 exponiendo I2C)
  • 1 × ATECC608A (co-procesador criptográfico I2C; breakout típico con GND/VCC/SDA/SCL)
  • Cables dupont macho-macho
  • 1 × Cable USB-C de datos

  • Opcionales para validación adicional:

  • 1 × Segundo nodo LoRa (otro ESP32 + RFM95W) para actuar como receptor
  • Analizador lógico o SDR (para verificar radiofrecuencia)
  • Acelerómetro calibrado o fuente de vibración (motor desbalanceado, mesa vibratoria)

Preparación y conexión

Mapa de pines y buses

  • Buses elegidos (coherentes con ESP32-S3-DevKitC-1 y el objetivo):
  • I2C0 para IMU y ATECC608A a 400 kHz
  • SDA = GPIO 8
  • SCL = GPIO 9
  • SPI para LoRa RFM95W (SX1276)
  • SCK = GPIO 12
  • MISO = GPIO 13
  • MOSI = GPIO 11
  • NSS/CS = GPIO 10
  • RST = GPIO 18
  • DIO0 = GPIO 17 (IRQ principal)
  • DIO1 = GPIO 16 (IRQ adicional, opcional)

  • Alimentación:

  • 3V3 desde la DevKitC-1 para RFM95W, ICM-42688-P y ATECC608A
  • GND común para todos

  • Dirección I2C:

  • ICM-42688-P en 0x68 (AD0 a GND; si AD0 a VCC → 0x69)
  • ATECC608A en 0x60 (típico)

Tabla de conexiones

Módulo Señal Pin en módulo Pin en ESP32-S3-DevKitC-1
RFM95W (SX1276) VCC 3V3 3V3
RFM95W (SX1276) GND GND GND
RFM95W (SX1276) SCK SCK GPIO 12
RFM95W (SX1276) MISO MISO GPIO 13
RFM95W (SX1276) MOSI MOSI GPIO 11
RFM95W (SX1276) NSS (CS) NSS GPIO 10
RFM95W (SX1276) RST RST GPIO 18
RFM95W (SX1276) DIO0 DIO0 GPIO 17
RFM95W (SX1276) DIO1 (opcional) DIO1 GPIO 16
ICM-42688-P VCC 3V3 3V3
ICM-42688-P GND GND GND
ICM-42688-P SDA SDA GPIO 8
ICM-42688-P SCL SCL GPIO 9
ICM-42688-P AD0 AD0 a GND (0x68)
ATECC608A VCC 3V3 3V3
ATECC608A GND GND GND
ATECC608A SDA SDA GPIO 8
ATECC608A SCL SCL GPIO 9

Notas:
– Evitamos pines de “strap” del ESP32-S3 (GPIO0, GPIO3, GPIO45, GPIO46) para señales que puedan quedar forzadas al arranque.
– USB nativo del S3 usa GPIO19/20 internamente; no los empleamos.
– Mantén cables SPI cortos y con GND cercano para LoRa. Para I2C, 10–20 cm máximo a 400 kHz.

Código completo

A continuación encontrarás:
1) platformio.ini con versiones fijadas y banderas clave para USB CDC.
2) main.cpp con:
– Inicialización del I2C, IMU ICM-42688-P.
– Inicialización de ATECC608A y lectura de número de serie (para ID de dispositivo).
– Inicialización de LoRa (SX1276) con RadioLib.
– Muestreo a ~1.6 kS/s, cálculo de características (RMS, crest factor, kurtosis, centróide espectral) usando arduinoFFT.
– Detección de anomalías por z-score vs. baseline.
– Envío LoRa de un payload binario compacto con ID, contador, features y flag.

platformio.ini

; platformio.ini — lora-predictive-vibration-icm42688
[env:esp32-s3-devkitc-1]
platform = espressif32 @ 6.6.0
board = esp32-s3-devkitc-1
framework = arduino

; Paquetes y toolchain fijados para reproducibilidad
platform_packages =
  framework-arduinoespressif32 @ 3.0.7
  toolchain-xtensa-esp32s3 @ 12.2.0+2023r1

; Librerías específicas
lib_deps =
  jgromes/RadioLib @ 6.5.0
  hideakitai/ICM42688 @ 0.2.2
  arduinoFFT @ 2.0.3
  microchip/cryptoauthlib @ 3.3.3

; Velocidad de monitor
monitor_speed = 115200

; Habilitar USB CDC en boot y seleccionar Serial por USB nativo
build_flags =
  -DARDUINO_USB_MODE=1
  -DARDUINO_USB_CDC_ON_BOOT=1
  -DCORE_DEBUG_LEVEL=3

; Región LoRa (ajusta a tu país/región)
; EU868: 868.1 MHz; US915: 915.0 MHz
build_unflags = -Werror

; Subir por USB (CDC)
upload_protocol = esptool
; En Linux suele ser /dev/ttyACM0 o /dev/ttyACM1
; upload_port = /dev/ttyACM0
; monitor_port = /dev/ttyACM0

; Fijar frecuencia de CPU si necesitas determinismo extra (opcional)
; board_build.f_cpu = 240000000L

src/main.cpp

#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <RadioLib.h>
#include <ICM42688.h>     // hideakitai/ICM42688
#include <arduinoFFT.h>   // v2.0.3
#include "cryptoauthlib.h"

// ---------------------- Pines ----------------------
#define LORA_SCK   12
#define LORA_MISO  13
#define LORA_MOSI  11
#define LORA_CS    10
#define LORA_RST   18
#define LORA_DIO0  17
#define LORA_DIO1  16

#define I2C_SDA     8
#define I2C_SCL     9

// ---------------------- Config LoRa ----------------
#ifndef LORA_FREQ_MHZ
  #define LORA_FREQ_MHZ 868.1  // Ajusta a tu región (EU868). US915 -> 915.0
#endif

// RadioLib: crear módulo SX1276 con SPI global y mapeo de pines
Module loraModule(LORA_CS, LORA_DIO0, LORA_RST, LORA_DIO1);
SX1276 radio(&loraModule);

// ---------------------- IMU ICM-42688-P -----------
ICM42688 imu(Wire);  // hideakitai: constructor por I2C
static const uint8_t ICM42688_ADDR = 0x68;  // AD0 a GND

// ---------------------- FFT y señal ----------------
constexpr size_t N_SAMPLES = 512;    // ventana (potencia de 2 para FFT)
constexpr float FS_HZ = 1600.0f;     // tasa de muestreo (IMU ODR >= 1.6kHz)
double vReal[N_SAMPLES];
double vImag[N_SAMPLES];
arduinoFFT FFT(vReal, vImag, N_SAMPLES, FS_HZ);

// ---------------------- Baseline/anomalía ----------
struct Features {
  float rms;
  float crest_factor;
  float kurtosis;
  float spectral_centroid;
};

// Baseline estadístico simple
Features baselineMean = {0};
Features baselineStd  = {1,1,1,1};
bool baselineReady = false;
uint16_t baselineWindows = 0;
const uint16_t WARMUP_WINDOWS = 20; // primeras ventanas para baseline

// ---------------------- ATECC608A ------------------
bool ateccReady = false;
uint8_t ateccSerial[9] = {0}; // 9 bytes serial

// ---------------------- Utilidad -------------------
uint32_t frameCounter = 0;

// Cuantización de features para payload
int16_t q15_from_float(float x, float scale) {
  float v = x * scale;
  if (v > 32767.0f) v = 32767.0f;
  if (v < -32768.0f) v = -32768.0f;
  return (int16_t)lroundf(v);
}

// ---------------------- ATECC: init y serial -------
bool initATECC() {
  ATCAIfaceCfg cfg = cfg_ateccx08a_i2c_default();
  // Ajustar I2C en ESP32-S3
  cfg.atcai2c.address = 0x60;
  cfg.atcai2c.bus = 0;      // Wire
  cfg.atcai2c.baud = 400000;

  ATCA_STATUS s = atcab_init(&cfg);
  if (s != ATCA_SUCCESS) {
    Serial.printf("ATECC init fallo: %d\n", s);
    return false;
  }
  s = atcab_read_serial_number(ateccSerial);
  if (s != ATCA_SUCCESS) {
    Serial.printf("ATECC read serial fallo: %d\n", s);
    return false;
  }
  Serial.print("ATECC608A S/N: ");
  for (int i = 0; i < 9; i++) {
    Serial.printf("%02X", ateccSerial[i]);
  }
  Serial.println();
  return true;
}

// ---------------------- IMU: init -------------------
bool initIMU() {
  Wire.begin(I2C_SDA, I2C_SCL, 400000); // 400 kHz
  delay(10);
  if (!imu.begin(ICM42688_ADDR)) {
    Serial.println("ICM-42688-P no responde en 0x68. Revisa cableado/AD0.");
    return false;
  }
  // Configurar ODR y rangos (consulta API de hideakitai/ICM42688)
  imu.setAccelODR(ICM42688::odr::odr1k6); // ~1.6 kHz
  imu.setGyroODR(ICM42688::odr::odr1k6);
  imu.setAccelRange(ICM42688::accel_range::g16); // mayor rango para vibración
  imu.setGyroRange(ICM42688::gyro_range::dps2000);
  imu.setAccelLPF(ICM42688::accel_lpf::lpf_180Hz);
  imu.setGyroLPF(ICM42688::gyro_lpf::lpf_180Hz);
  imu.enableAccel(true);
  imu.enableGyro(false); // enfocamos vibración en acelerómetro
  delay(50);
  Serial.println("ICM-42688-P inicializado.");
  return true;
}

// ---------------------- LoRa: init ------------------
bool initLoRa() {
  // Inicializar SPI con pines definidos
  SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);

  int8_t state = radio.begin(LORA_FREQ_MHZ, 125.0, 9, 5, 0x34, 14, 8, 0);
  // Parámetros: freq MHz, BW kHz, SF, CR denom, syncWord, power dBm, preamble len, gain
  // Ajustados a SF9 para sensibilidad/balance
  if (state != RADIOLIB_ERR_NONE) {
    Serial.printf("LoRa init fallo, code %d\n", state);
    return false;
  }
  // Opcionales: fijar tiempo de transmisión (TCXO no usado), cabecera explícita, CRC
  radio.setCRC(true);
  Serial.println("SX1276 (RFM95W) inicializado.");
  return true;
}

// ------------------- Muestreo y features ------------
// Calcula features sobre vReal (una ventana) y rellena spectral centroid
Features computeFeatures() {
  // 1) RMS, crest factor, kurtosis (dominio tiempo)
  double mean = 0, sum2 = 0, maxAbs = 0, sum4 = 0;
  for (size_t i = 0; i < N_SAMPLES; i++) {
    double x = vReal[i];
    mean += x;
    sum2 += x * x;
    double ax = fabs(x);
    if (ax > maxAbs) maxAbs = ax;
  }
  mean /= N_SAMPLES;
  double var = 0;
  for (size_t i = 0; i < N_SAMPLES; i++) {
    double d = vReal[i] - mean;
    var += d * d;
    sum4 += d * d * d * d;
  }
  var /= N_SAMPLES;
  double rms = sqrt(sum2 / N_SAMPLES);
  double sigma = sqrt(var + 1e-12);
  double kurt = (sum4 / N_SAMPLES) / (pow(sigma, 4) + 1e-12);

  // crest factor = max(|x|)/RMS
  double crest = (rms > 1e-9) ? (maxAbs / rms) : 0.0;

  // 2) FFT y centróide espectral
  // Copiamos vReal a arrays de FFT (ya en vReal, vImag=0)
  for (size_t i = 0; i < N_SAMPLES; i++) vImag[i] = 0.0;
  // Ventana Hanning para reducir leakage
  for (size_t i = 0; i < N_SAMPLES; i++) {
    double w = 0.5 * (1 - cos(2.0 * PI * i / (N_SAMPLES - 1)));
    vReal[i] = vReal[i] * w;
  }
  FFT.windowing(FFT_WIN_TYP_RECTANGLE, FFT_FORWARD); // ya aplicamos Hanning manual, mantenemos RECT
  FFT.compute(FFT_FORWARD);
  FFT.complexToMagnitude();

  double num = 0, den = 0;
  const double binHz = FS_HZ / N_SAMPLES;
  // Usamos bins 1..N/2-1 (excluye DC y Nyquist)
  for (size_t k = 1; k < N_SAMPLES/2; k++) {
    double f = k * binHz;
    double a = vReal[k];
    num += f * a;
    den += a + 1e-12;
  }
  double sc = num / den;

  Features feat;
  feat.rms = (float)rms;
  feat.crest_factor = (float)crest;
  feat.kurtosis = (float)kurt;
  feat.spectral_centroid = (float)sc;
  return feat;
}

// Z-score simple por campo
float zscore(float x, float mu, float sigma) {
  return (x - mu) / (sigma + 1e-9f);
}

void updateBaseline(const Features& f) {
  // Promedio incremental y desviación (Welford simplificado por campo)
  // Para robustez en demo, asumimos baselineStd constante aprox. y la recalculamos tras calentamiento.
  baselineWindows++;
  // Media incremental
  baselineMean.rms += (f.rms - baselineMean.rms) / baselineWindows;
  baselineMean.crest_factor += (f.crest_factor - baselineMean.crest_factor) / baselineWindows;
  baselineMean.kurtosis += (f.kurtosis - baselineMean.kurtosis) / baselineWindows;
  baselineMean.spectral_centroid += (f.spectral_centroid - baselineMean.spectral_centroid) / baselineWindows;

  // Tras WARMUP, podrías recalcular std con un buffer. Aquí, aproximamos tras estabilizar:
  if (baselineWindows == WARMUP_WINDOWS) {
    // Marcamos baseline ready; en práctica, acumula ventanas para std real.
    baselineStd = { baselineMean.rms * 0.1f + 1e-3f,
                    baselineMean.crest_factor * 0.1f + 1e-3f,
                    baselineMean.kurtosis * 0.1f + 1e-3f,
                    baselineMean.spectral_centroid * 0.1f + 1e-3f };
    baselineReady = true;
    Serial.println("Baseline listo.");
  }
}

// ---------------------- Payload ---------------------
// Estructura binaria compacta (little-endian)
// [0..3]   dev_id (uint32)   -> derivado de serial ATECC
// [4..7]   frameCounter (u32)
// [8..9]   q_rms (q15, *1000)
// [10..11] q_crest (q15, *1000)
// [12..13] q_kurt (q15, *1000)
// [14..15] q_sc (q15, *1)  (Hz, recortado/escala adecuada)
// [16]     flags (bit0=anomaly)
// Longitud: 17 bytes
struct __attribute__((packed)) Packet {
  uint32_t dev_id;
  uint32_t frame;
  int16_t q_rms;
  int16_t q_crest;
  int16_t q_kurt;
  int16_t q_sc;
  uint8_t flags;
};

uint32_t devIdFromATECC() {
  // Usa 4 bytes del serial (evita 0)
  uint32_t id = 0xA5A50000;
  if (ateccReady) {
    id = ((uint32_t)ateccSerial[5] << 24) |
         ((uint32_t)ateccSerial[6] << 16) |
         ((uint32_t)ateccSerial[7] << 8)  |
         ((uint32_t)ateccSerial[8]);
  }
  if (id == 0) id = 0xA5A5DEAD;
  return id;
}

// ---------------------- Setup/Loop ------------------
void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println("\n[ESP32-S3] lora-predictive-vibration-icm42688");

  // I2C + IMU
  if (!initIMU()) {
    Serial.println("Fallo IMU. Corrige y reinicia.");
  }

  // ATECC608A
  ateccReady = initATECC();

  // LoRa
  if (!initLoRa()) {
    Serial.println("Fallo LoRa. Corrige y reinicia.");
  }

  // Sincroniza primer baseline
  baselineWindows = 0;
  baselineReady = false;
}

void loop() {
  // 1) Muestreo N_SAMPLES a ~FS_HZ usando lectura rápida del acelerómetro
  const float Ts_us = 1e6f / FS_HZ;
  uint32_t t0 = micros();
  for (size_t i = 0; i < N_SAMPLES; i++) {
    // Lectura de aceleración (g). Con hideakitai/ICM42688: imu.getAccelX/Y/Z()
    imu.update(); // refresca datos internamente
    float ax = imu.getAccelX(); // g
    // Puedes combinar magnitud o eje con mayor energía. Usamos |ax| centrado.
    vReal[i] = (double)ax; // g
    vImag[i] = 0.0;
    // Espera cronometrada
    uint32_t tNext = t0 + (uint32_t)((i + 1) * Ts_us);
    while ((int32_t)(micros() - tNext) < 0) { /* busy-wait corto */ }
  }

  // 2) Calcular features
  Features f = computeFeatures();

  // 3) Actualizar baseline (fase de calentamiento)
  if (!baselineReady) {
    updateBaseline(f);
  }

  // 4) Detección de anomalía simple (z-score combinado)
  float zr = zscore(f.rms, baselineMean.rms, baselineStd.rms);
  float zc = zscore(f.crest_factor, baselineMean.crest_factor, baselineStd.crest_factor);
  float zk = zscore(f.kurtosis, baselineMean.kurtosis, baselineStd.kurtosis);
  float zs = zscore(f.spectral_centroid, baselineMean.spectral_centroid, baselineStd.spectral_centroid);

  // Umbral combinado (p. ej., cualquier |z| > 3)
  bool anomaly = baselineReady && (fabs(zr) > 3.0f || fabs(zc) > 3.0f || fabs(zk) > 3.0f || fabs(zs) > 3.0f);

  // 5) Construir y enviar payload LoRa
  Packet pkt;
  pkt.dev_id = devIdFromATECC();
  pkt.frame = frameCounter++;
  pkt.q_rms   = q15_from_float(f.rms, 1000.0f);           // g -> mg
  pkt.q_crest = q15_from_float(f.crest_factor, 1000.0f);  // adim
  // limita kurtosis razonable (0..20)
  float kurt_limited = fminf(fmaxf(f.kurtosis, 0.0f), 20.0f);
  pkt.q_kurt  = q15_from_float(kurt_limited, 1000.0f);
  // centróide en Hz, suponiendo < 1600 Hz -> escala 1
  float sc_limited = fminf(fmaxf(f.spectral_centroid, 0.0f), 1600.0f);
  pkt.q_sc    = q15_from_float(sc_limited, 1.0f);
  pkt.flags   = (anomaly ? 0x01 : 0x00);

  int16_t state = radio.transmit((uint8_t*)&pkt, sizeof(pkt));
  if (state == RADIOLIB_ERR_NONE) {
    Serial.printf("#%lu LoRa TX OK | RMS=%.4f g, CF=%.3f, K=%.3f, SC=%.1f Hz | anomaly=%d\n",
      (unsigned long)pkt.frame, f.rms, f.crest_factor, f.kurtosis, f.spectral_centroid, anomaly);
  } else {
    Serial.printf("LoRa TX fallo, code %d\n", state);
  }

  // 6) Respeta duty-cycle (ej., 1 envío por segundo aprox.)
  delay(1000);
}

Explicación breve de partes clave:
– Configuración de pines/buses: se fija I2C en SDA=8, SCL=9; y SPI en SCK=12/MISO=13/MOSI=11/CS=10, con RST y DIOs para el RFM95W.
– IMU: se configura ODR de 1.6 kHz y se habilita acelerómetro con LPF ~180 Hz para capturar vibraciones relevantes (puedes modificar ODR/LPF según tu máquina).
– Muestreo: se adquiere N_SAMPLES=512 puntos a ~1.6 kHz (≈320 ms de ventana).
– Features: se calculan RMS, crest factor, kurtosis (dominio del tiempo) y centróide espectral (dominio de frecuencia mediante FFT).
– Baseline: primeras 20 ventanas estiman medias y desviaciones, para luego calcular z-scores.
– Anomalía: gatilla si cualquier |z| > 3.
– ATECC608A: se lee el número de serie para derivar un dev_id estable. Si ya tienes el ATECC provisionado con clave, puedes firmar el hash del payload antes de transmitir (se deja fuera por simplicidad operativa).
– LoRa: RadioLib facilita setear frecuencia, BW=125 kHz, SF=9 (ajusta según SNR/datarate necesarios).

Compilación, flash y ejecución

1) Instala PlatformIO Core (CLI) si aún no lo tienes:
– Windows:
– pipx install platformio o py -m pip install –user platformio
– Ubuntu/macOS:
– python3 -m pip install –user platformio
– Verifica:
– pio –version → debería mostrar “PlatformIO Core, Home, Python…”

2) Crea el proyecto:
– pio project init –board esp32-s3-devkitc-1
– Sustituye el platformio.ini generado por el que se muestra arriba.
– Crea src/main.cpp y pega el código anterior.

3) Conecta la ESP32-S3-DevKitC-1 por USB-C. En Linux:
– ls /dev/ttyACM* → debería ver /dev/ttyACM0
– Si no, agrega permisos: sudo usermod -a -G dialout $USER y relogin.

4) Compila:
– pio run

5) Sube firmware:
– pio run -t upload
– Si necesitas especificar puerto: pio run -t upload -e esp32-s3-devkitc-1 –upload-port /dev/ttyACM0

6) Abre monitor serie:
– pio device monitor –baud 115200
– Si el puerto cambia tras reset, especifica: pio device monitor –port /dev/ttyACM0 –baud 115200

7) Salida esperada al iniciar:
– [ESP32-S3] lora-predictive-vibration-icm42688
– ICM-42688-P inicializado.
– ATECC608A S/N: XXXXXXX…
– SX1276 (RFM95W) inicializado.
– Baseline listo. (tras ~20 ventanas)

Validación paso a paso

1) Validación de hardware (sin firmware):
– Verifica continuidad GND común.
– Verifica 3V3 estable en VCC de RFM95W, ICM-42688-P y ATECC608A.
– Comprueba la dirección I2C con un escáner simple (opcional): debería ver 0x68 y 0x60.

2) Validación de inicialización:
– En el monitor serie, confirma los mensajes:
– “ICM-42688-P inicializado.”
– “ATECC608A S/N: …”
– “SX1276 (RFM95W) inicializado.”
– Si falla alguno, revisa el apartado Troubleshooting.

3) Validación de muestreo/FFT:
– Con el dispositivo inmóvil, observa:
– RMS bajo (p. ej., < 0.02 g), centróide ~ muy bajo (ruido).
– Kurtosis cerca de 3 (gaussiano ideal).
– Induce vibración (pe. motor con desequilibrio, golpe suave):
– RMS sube.
– Crest factor sube si hay picos.
– Centróide se desplaza hacia la frecuencia dominante.

4) Validación de baseline/anomalía:
– Tras “Baseline listo.”, repite la inducción de vibración:
– Observa anomaly=1 cuando el z-score supera el umbral.
– Si no se gatilla, incrementa sensibilidad (reduce baselineStd o baja umbral a 2.5).

5) Validación de LoRa TX:
– En el monitor serie: “LoRa TX OK” por cada frame.
– Con un receptor LoRa (opcional), configura la misma modulación (BW=125 kHz, SF=9, CR=4/5, SyncWord=0x34, frecuencia regional) y captura payload de 17 bytes.
– Decodifica el payload (dev_id, frame, q_rms mg, q_crest1e-3, q_kurt1e-3, q_sc Hz, flags).
– Si no tienes receptor, al menos confirma que la transmisión no da error y que el LED en el RFM95W (si existe) muestra actividad RF (no todos los módulos tienen LED).

6) Validación de ATECC:
– Verifica que el S/N impreso permanece constante entre reinicios.
– Opcional: prueba una llamada atcab_random() y muestra 32 bytes aleatorios (para confirmar crypto HAL). No usado en el payload base.

7) Medidas cuantitativas de referencia:
– Inmóvil:
– RMS ~ 0.005–0.02 g
– CF ~ 1.2–2.0
– K ~ 2.5–3.5
– SC ~ < 50 Hz
– Vibrando:
– RMS > 0.05 g (según condición)
– CF puede subir a 3–8 si hay impactos.
– SC tiende a la frecuencia de vibración (100–300 Hz típico en pequeños motores, o acorde a tu excitación).

Troubleshooting

1) No aparece puerto serie / no sube el firmware:
– Revisa cable USB (que soporte datos).
– Prueba otro puerto USB del equipo.
– Mantén pulsado BOOT al reset para forzar modo descarga (si aplica).
– En Linux: agrega tu usuario a dialout; asegura permisos en /dev/ttyACM*.
– En macOS, cierra apps que capturan el puerto (p. ej., otro monitor serie).

2) “ICM-42688-P no responde en 0x68”:
– Revisa SDA=GPIO8, SCL=GPIO9, VCC=3V3, GND.
– Asegúrate de que AD0 está a GND si usas 0x68 (o cambia la dirección en el código a 0x69).
– Verifica que Wire.begin(I2C_SDA, I2C_SCL, 400000) no sea llamado dos veces con otros pines.
– Baja la velocidad I2C a 100 kHz en casos de cables largos.

3) ATECC init fallo (códigos – falla en atcab_init o read serial):
– Confirma SDA/SCL compartidos correctamente con IMU.
– Verifica dirección 0x60.
– Algunos breakouts requieren pull-ups a 3.3V (4.7 kΩ). Asegura que existan (al menos en un dispositivo del bus).
– Si persiste, prueba a inicializarlo antes del IMU (orden de init a veces afecta en bus compartido).

4) LoRa init fallo, code -2/-5 (RadioLib):
– Verifica cableado SPI (SCK=12, MISO=13, MOSI=11, CS=10, RST=18, DIO0=17).
– Asegura que SPI.begin() usa esos pines.
– Revisa que VCC=3.3V (no 5V) y consumo suficiente (≈120 mA en TX).
– Cambia la frecuencia a la adecuada para tu módulo/región (p. ej., 915.0 para US).

5) LoRa TX fallo al transmitir:
– Reduce potencia (radio.setOutputPower(10)) si tu alimentación es marginal.
– Aumenta preámbulo (radio.setPreambleLength(12)) para robustez.
– Asegura antena adecuada a la banda (SWR razonable).

6) Lecturas de IMU ruidosas o saturadas:
– Baja el rango a ±4 g si saturas menos (setAccelRange(g4)) o súbelo a ±16 g si saturas por impactos.
– Ajusta LPF (100–180 Hz típicos, según vibración de interés).
– Monta mecánicamente la IMU con tornillos o cinta de espuma para evitar resonancias parásitas.

7) Baseline nunca “listo” o demasiados falsos positivos:
– Aumenta WARMUP_WINDOWS (p. ej., 50).
– Calcula std real con un buffer deslizante (mejora sugerida abajo).
– Subir umbral a 3.5 o 4 si hay demasiado ruido.

8) El monitor serie no muestra nada:
– Asegura build_flags con USB CDC on boot.
– Cambia monitor_port al dispositivo correcto (Windows: COMx; Linux: /dev/ttyACM0).
– Pulsa RESET y reabre monitor.

Mejoras/variantes

  • LoRaWAN (The Things Network):
  • Sustituir RadioLib por una pila LoRaWAN (p. ej., mcci-catena LMIC o RadioLib LoRaWAN si soportado).
  • ATECC608A para almacenar AppKey/NwkKey y hacer join seguro.
  • Cuidado con duty-cycle y tamaño de payload (codificar features de manera compacta).

  • Mejor baseline/anomalía:

  • Mantener un buffer circular de M ventanas y calcular media/desviación robustas (mediana/MAD).
  • Modelado espectral: comparar picos dominantes y su desplazamiento a lo largo del tiempo.
  • One-class SVM o autoencoders con cuantización fija (TinyML). EloquentTinyML o TFLM si te mantienes en 20–30 kB de RAM para el modelo.

  • Ampliación de características:

  • Band energy ratios (BER) para bandas [0–100, 100–300, 300–600 Hz…].
  • Envolvente Hilbert o demodulación para vibración de rodamientos.
  • Kurtosis espectral, skewness, picos armónicos.

  • ATECC608A: autenticidad e integridad:

  • Firmar SHA-256 del payload con atcab_sign() si la clave privada está provisionada en Slot 0.
  • En el receptor, validar con la clave pública correspondiente.
  • Añadir un nonce/challenge para evitar repetición.

  • Gestión de energía:

  • Dormir entre ventanas y despertar con temporizador.
  • Ajustar potencia LoRa y SF según calidad de enlace medido.

  • Robustez RF:

  • Saltos de frecuencia (FHSS simple a nivel de aplicación).
  • Retransmisión con ACK por un receptor si implementas enlace a medida.

  • Calibración y alineación:

  • Compensación de gravedad (restar media DC por eje).
  • Montaje con orientación conocida (usar combinaciones ax/ay/az o magnitud sqrt).

Checklist de verificación

  • [ ] He instalado PlatformIO Core 6.1.15 y verificado pio –version.
  • [ ] platformio.ini usa platform = espressif32 @ 6.6.0 y framework-arduinoespressif32 @ 3.0.7.
  • [ ] He cableado I2C en SDA=GPIO8, SCL=GPIO9 para ICM-42688-P (0x68) y ATECC608A (0x60).
  • [ ] He cableado SPI LoRa con SCK=12, MISO=13, MOSI=11, CS=10, RST=18, DIO0=17, DIO1=16.
  • [ ] Compila sin errores: pio run OK.
  • [ ] Carga sin errores: pio run -t upload OK.
  • [ ] Monitor serie muestra inicializaciones correctas (IMU, ATECC S/N, LoRa).
  • [ ] Tras 20 ventanas, aparece “Baseline listo.”.
  • [ ] Induciendo vibración, veo RMS/CF/SC aumentar y a veces anomaly=1.
  • [ ] Veo “LoRa TX OK” por frame (1 Hz aprox.).
  • [ ] (Opcional) Un receptor LoRa con misma modulación ve payloads de 17 bytes coherentes.

Notas finales:
– Este caso práctico mantiene coherencia total con el hardware “ESP32-S3-DevKitC-1 + RFM95W (SX1276) + ICM-42688-P + ATECC608A”, en materiales, conexiones, código y comandos.
– La cadena de herramientas exacta y las versiones fijadas garantizan reproducibilidad. Si alguna versión cambia, mantén los mismos números en platformio.ini para evitar variaciones del entorno.
– Ajusta la frecuencia LoRa a tu región y respeta la normativa local (duty-cycle, potencia, bandas).

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Qué versión de Python se debe utilizar en Windows?




Pregunta 2: ¿Cuál es la versión de PlatformIO Core que se debe usar?




Pregunta 3: ¿Qué librería se utiliza para el manejo de SX1276/RFM95W?




Pregunta 4: ¿Qué plataforma se requiere para PlatformIO?




Pregunta 5: ¿Cuál es la versión del framework de Arduino para ESP32?




Pregunta 6: ¿Qué controlador USB se necesita si se usa un adaptador USB-UART alternativo en Windows?




Pregunta 7: ¿Qué comando se utiliza para verificar la versión de PlatformIO Core?




Pregunta 8: ¿Qué debe hacerse en Linux para acceder a /dev/ttyACM*?




Pregunta 9: ¿Qué tipo de USB utiliza la ESP32-S3-DevKitC-1 para programarse?




Pregunta 10: ¿Cuál es la versión del adaptador CH34x para Windows/macOS?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme: