Caso práctico: Visualizador de espectro I2S en tiempo real

Caso práctico: Visualizador de espectro I2S en tiempo real — hero

Objetivo y caso de uso

Qué construirás: Un visualizador de espectro de audio en tiempo real utilizando un Arduino Nano RP2040 Connect, un micrófono INMP441 y una pantalla ILI9341.

Para qué sirve

  • Visualizar en tiempo real la amplitud de diferentes frecuencias de audio capturadas por el micrófono INMP441.
  • Monitorear la calidad del sonido en entornos de grabación o eventos en vivo.
  • Crear una herramienta educativa para entender el espectro de audio y sus componentes.
  • Implementar un sistema de alerta visual para niveles de sonido peligrosos en ambientes industriales.

Resultado esperado

  • Visualización fluida de espectros de audio con actualizaciones en tiempo real a 30 FPS.
  • Medición de latencias de procesamiento de audio inferiores a 50 ms.
  • Capacidad de detectar y mostrar frecuencias de hasta 20 kHz con precisión.
  • Generación de informes de niveles de sonido en dB con un rango de 0 a 120 dB.

Público objetivo: Ingenieros de audio, estudiantes de electrónica; Nivel: Avanzado

Arquitectura/flujo: Captura de audio -> Procesamiento I2S -> Visualización en pantalla ILI9341.

Nivel: Avanzado

Prerrequisitos

Sistema operativo (probado)

  • Linux: Ubuntu 22.04 LTS (kernel 5.15.x)
  • Windows: Windows 11 23H2 (Build 22631.x)
  • macOS: Sonoma 14.5 (Apple Silicon o Intel)

Nota: El procedimiento y los comandos se muestran principalmente para Linux/macOS (bash). En Windows PowerShell/CMD son equivalentes cambiando rutas y comillas donde corresponda.

Toolchain exacta (versiones fijadas)

  • Arduino CLI: 0.35.3
  • Core para RP2040 (Earle Philhower): rp2040:rp2040 4.1.2
  • Bibliotecas Arduino:
  • I2S (incluida en el core rp2040:rp2040 4.1.2)
  • Adafruit GFX Library 1.11.9
  • Adafruit ILI9341 1.6.1
  • arduinoFFT 1.6.0

Drivers y puertos

  • Arduino Nano RP2040 Connect usa USB CDC-ACM nativo. En:
  • Linux/macOS: no requiere controladores adicionales.
  • Windows 10/11: no requiere CP210x/CH34x (no aplican a este modelo). Si el puerto no aparece, actualizar Windows Update o instalar “Arduino Mbed OS RP2040 Boards” desde el IDE para forzar la instalación del driver CDC-ACM firmado.
  • Puerto serie típico:
  • Linux: /dev/ttyACM0, /dev/ttyACM1
  • macOS: /dev/cu.usbmodemXXXX
  • Windows: COM3, COM4, etc.

Materiales

  • 1x Arduino Nano RP2040 Connect (modelo exacto)
  • 1x Micrófono I2S INMP441 (módulo breakout 3.3 V)
  • 1x Pantalla TFT ILI9341 2.4″/2.8″ SPI (3.3 V, controlador ILI9341)
  • Cables dupont hembra-hembra
  • Resistencia 100–220 Ω para el pin LED/BLED de la pantalla (retroiluminación)
  • Cable USB-C o Micro-USB según la placa (Nano RP2040 Connect utiliza Micro-USB)
  • Fuente: Alimentación USB del PC (5 V). Todos los periféricos funcionan a 3.3 V.

Observaciones importantes:
– El Arduino Nano RP2040 Connect trabaja a 3.3 V en sus GPIO. INMP441 e ILI9341 aceptan 3.3 V, por lo que no se requieren conversores de nivel.
– El INMP441 requiere líneas I2S: BCLK (SCK), LRCLK/WS y SD (datos desde micrófono al MCU).
– La pantalla ILI9341 usa SPI: SCK, MOSI, MISO (opcional para lectura), CS, DC, RST, LED.

Preparación y conexión

Esquema de pines (enlazado con el código)

Seleccionaremos pines del Nano RP2040 Connect que sean cómodos y queden libres de funciones especiales usadas por el core:

  • I2S INMP441:
  • BCLK (SCK): D3
  • LRCLK (WS): D2
  • SD (Datos IN): D4
  • L/R: GND (canal izquierdo)
  • VDD: 3.3V
  • GND: GND

  • ILI9341 (SPI):

  • CS: D10
  • DC: D9
  • RST: D8
  • MOSI: D11 (SPI MOSI)
  • MISO: D12 (SPI MISO; no imprescindible)
  • SCK: D13 (SPI SCK)
  • LED/BLED: 3.3V mediante resistencia 100–220 Ω
  • VCC: 3.3V
  • GND: GND

Tabla de cableado detallada:

Dispositivo Señal/Pin Nano RP2040 Connect Comentario
INMP441 VDD 3.3V Alimentación 3.3V
INMP441 GND GND Referencia común
INMP441 SCK/BCLK D3 BCLK I2S (entrada mic de MCU)
INMP441 WS/LRCLK D2 Word Select I2S
INMP441 SD D4 Datos desde micrófono
INMP441 L/R GND Seleccionar canal izquierdo
ILI9341 VCC 3.3V Alimentación 3.3V
ILI9341 GND GND Referencia común
ILI9341 CS D10 Chip Select
ILI9341 DC D9 Data/Command
ILI9341 RST D8 Reset de la pantalla
ILI9341 MOSI D11 SPI MOSI (salida del MCU)
ILI9341 MISO D12 SPI MISO (no usado para dibujar)
ILI9341 SCK D13 SPI SCK
ILI9341 LED/BLED 3.3V a través de 100–220 Ω Retroiluminación

Notas de conexión:
– Conecta GND común entre los tres módulos.
– INMP441 es sensible a ruidos de reloj; mantén los cables BCLK/LRCLK/SD cortos y enrutados juntos si es posible.
– La retroiluminación (LED/BLED) puede conectarse al 3.3V con la resistencia recomendada. Si la pantalla no enciende, revisa este punto.

Código completo (C++ para Arduino RP2040)

Objetivo: capturar audio en tiempo real desde el INMP441 vía I2S, realizar una FFT, y pintar un visualizador de barras del espectro en el ILI9341 con escala logarítmica de frecuencia y caída suave de los picos.

Características clave:
– Toma N=1024 muestras a Fs=16000 Hz (resolución de bin ≈ 15.625 Hz).
– Ventaneo Hann para reducir fugas espectrales.
– Mapeo logarítmico de bins a ~64 barras.
– Renderizado con Adafruit GFX + ILI9341 usando SPI hardware.
– Control de fps para evitar parpadeos.

Sketch principal

// i2s-spectrum-visualizer.ino
// Dispositivo: Arduino Nano RP2040 Connect + INMP441 + ILI9341
// Toolchain: Arduino CLI 0.35.3 + rp2040:rp2040@4.1.2
// Librerías: Adafruit_GFX 1.11.9, Adafruit_ILI9341 1.6.1, arduinoFFT 1.6.0

#include <Arduino.h>
#include <I2S.h>                // Provista por rp2040:rp2040 (Earle Philhower)
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <arduinoFFT.h>

// ------- Configuración de pines -------
static const int PIN_I2S_BCLK = 3;   // INMP441 SCK/BCLK -> D3
static const int PIN_I2S_LRCLK = 2;  // INMP441 WS/LRCLK -> D2
static const int PIN_I2S_SD = 4;     // INMP441 SD -> D4

static const int TFT_CS  = 10;       // ILI9341 CS -> D10
static const int TFT_DC  = 9;        // ILI9341 DC -> D9
static const int TFT_RST = 8;        // ILI9341 RST -> D8

// ------- Configuración de audio/FFT -------
static const uint32_t SAMPLE_RATE = 16000; // Hz
static const uint16_t N_SAMPLES   = 1024;  // Potencia de 2 para FFT
static const uint8_t  N_BARS      = 64;    // Número de barras a dibujar

// Bufferes para FFT
double vReal[N_SAMPLES];
double vImag[N_SAMPLES];
arduinoFFT FFT(vReal, vImag, N_SAMPLES, SAMPLE_RATE);

// I2S: entrada únicamente (del mic hacia MCU)
I2S i2s(INPUT);

// Pantalla
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);

// Variables para renderizado
uint16_t screenW, screenH;
uint16_t barWidth;
uint8_t  margin = 2; // px entre barras
float    peakHold[N_BARS];
uint32_t lastDraw = 0;
const    uint16_t targetFPS = 30;

// Tabla Hann precalculada
float hann[N_SAMPLES];

// Asignación logarítmica de bins a barras
uint16_t binIdxStart[N_BARS + 1]; // rangos de bins por barra

// Funciones auxiliares
void initHann() {
  for (uint16_t i = 0; i < N_SAMPLES; i++) {
    hann[i] = 0.5f * (1.0f - cosf((2.0f * PI * i) / (N_SAMPLES - 1)));
  }
}

void initLogBins() {
  // Frecuencias de 20 Hz a 8 kHz aprox. (limitadas por Fs/2 = 8 kHz)
  // Mapeo logarítmico para N_BARS barras.
  float fMin = 20.0f;
  float fMax = SAMPLE_RATE / 2.0f; // 8 kHz
  for (uint8_t b = 0; b <= N_BARS; b++) {
    float p = (float)b / (float)N_BARS;
    float f = fMin * powf(fMax / fMin, p);
    uint16_t k = (uint16_t)roundf((f * N_SAMPLES) / SAMPLE_RATE);
    if (k >= N_SAMPLES / 2) k = (N_SAMPLES / 2) - 1; // Nyquist-1
    binIdxStart[b] = k;
  }
  // Asegurar monotonía estricta
  for (uint8_t b = 1; b <= N_BARS; b++) {
    if (binIdxStart[b] <= binIdxStart[b - 1]) {
      binIdxStart[b] = binIdxStart[b - 1] + 1;
    }
    if (binIdxStart[b] >= (N_SAMPLES / 2)) {
      binIdxStart[b] = (N_SAMPLES / 2) - 1;
    }
  }
}

void drawGrid() {
  tft.fillScreen(ILI9341_BLACK);
  // Ejes y líneas guía sutiles
  uint16_t h = tft.height();
  uint16_t w = tft.width();
  for (int y = h - 1; y > 0; y -= h / 8) {
    tft.drawFastHLine(0, y, w, ILI9341_DARKGREY);
  }
  // Etiquetas de frecuencia aproximadas (opcional simplificado)
  tft.setTextColor(ILI9341_WHITE);
  tft.setTextSize(1);
  tft.setCursor(4, 4);
  tft.print("i2s-spectrum-visualizer");
}

void renderBars(const float *bars, const float *peaks) {
  for (uint8_t i = 0; i < N_BARS; i++) {
    uint16_t x = i * barWidth;
    uint16_t h = tft.height();
    // Altura normalizada a pantalla (0..1 -> 0..h)
    uint16_t barH = (uint16_t)constrain(bars[i] * (h - 1), 0, h - 1);
    uint16_t peakY = h - 1 - (uint16_t)constrain(peaks[i] * (h - 1), 0, h - 1);

    // Borrar columna
    tft.fillRect(x, 0, barWidth - margin, h, ILI9341_BLACK);

    // Color en gradiente simple por altura
    uint16_t color = (barH > (h * 0.75)) ? ILI9341_RED :
                     (barH > (h * 0.50)) ? ILI9341_ORANGE :
                     (barH > (h * 0.25)) ? ILI9341_YELLOW : ILI9341_GREEN;

    // Dibujar barra (desde la base)
    if (barH > 0) {
      tft.fillRect(x, h - barH, barWidth - margin, barH, color);
    }

    // Dibujar marcador de pico
    tft.drawFastHLine(x, peakY, barWidth - margin, ILI9341_CYAN);
  }
}

// Lee N_SAMPLES muestras de I2S y las coloca en vReal (como double)
bool captureSamples() {
  uint16_t captured = 0;
  const uint32_t timeoutMs = 50;
  uint32_t start = millis();
  while (captured < N_SAMPLES) {
    if ((millis() - start) > timeoutMs) return false; // evitar bloqueo
    int32_t sample = 0;
    int available = i2s.available();
    if (available >= 4) { // Lectura de 32-bit
      // La librería I2S entrega datos 24/32 bits firmados según mic
      // Leemos 32 bits y reescalamos a float/double
      sample = i2s.read(); // int32_t
      // INMP441: datos válidos en 24 bits MSB-alineados -> desplazar si es necesario
      // Normalizamos a rango [-1, 1] aproximadamente
      float s = (float)sample / 8388608.0f; // 2^23 (24-bit signed)
      vReal[captured] = (double)s * hann[captured]; // aplicar ventana
      vImag[captured] = 0.0;
      captured++;
    }
  }
  return true;
}

void computeSpectrum(float *outBars) {
  // FFT
  FFT.Windowing(FFT_WIN_TYP_NONE, FFT_FORWARD); // ya aplicamos ventana Hann manual
  FFT.Compute(FFT_FORWARD);
  FFT.ComplexToMagnitude();

  // Magnitud normalizada: bins [1 .. N/2-1]
  // Acumular energy por banda logarítmica
  for (uint8_t b = 0; b < N_BARS; b++) {
    uint16_t kStart = (b == 0) ? 1 : binIdxStart[b];
    uint16_t kEnd   = binIdxStart[b + 1];
    if (kEnd <= kStart) kEnd = kStart + 1;
    double sum = 0.0;
    for (uint16_t k = kStart; k < kEnd; k++) {
      double mag = vReal[k]; // tras ComplexToMagnitude: vReal[] contiene magnitudes
      sum += mag * mag;      // potencia
    }
    double rms = sqrt(sum / (double)(kEnd - kStart));
    // Compresión logarítmica para visualización
    float val = log10f(1.0f + (float)rms) * 1.8f; // factor empírico
    // Limitar a [0, 1]
    if (val > 1.0f) val = 1.0f;
    if (val < 0.0f) val = 0.0f;
    outBars[b] = val;
  }
}

void setup() {
  // Serial opcional para debug
  Serial.begin(115200);
  delay(200);

  // Pantalla
  tft.begin();
  tft.setRotation(1); // 320x240 horizontal
  screenW = tft.width();
  screenH = tft.height();
  barWidth = screenW / N_BARS;
  drawGrid();

  // Ventana Hann y mapeo logarítmico de bins
  initHann();
  initLogBins();
  for (uint8_t i = 0; i < N_BARS; i++) peakHold[i] = 0.0f;

  // I2S: configurar pines y formato
  i2s.setBCLK(PIN_I2S_BCLK);
  i2s.setLRCLK(PIN_I2S_LRCLK);
  i2s.setDATA(PIN_I2S_SD);
  // Formato típico para INMP441: I2S estándar (Philips), 32 bits por muestra (24 bits válidos)
  if (!i2s.begin(SAMPLE_RATE)) {
    // Algunas versiones usan begin(mode, fs, bits)
    // i2s.begin(I2S_PHILIPS_MODE, SAMPLE_RATE, 32);
    tft.setCursor(0, 20);
    tft.setTextColor(ILI9341_RED);
    tft.setTextSize(2);
    tft.println("Error I2S.begin()");
    while (1) { delay(1000); }
  }
}

void loop() {
  // Control de FPS
  uint32_t now = millis();
  uint32_t frameTime = 1000 / targetFPS;
  if (now - lastDraw < frameTime) return;
  lastDraw = now;

  // Captura I2S
  bool ok = captureSamples();
  if (!ok) {
    // Mostrar advertencia en pantalla
    tft.setCursor(0, 20);
    tft.setTextColor(ILI9341_RED, ILI9341_BLACK);
    tft.setTextSize(1);
    tft.println("I2S timeout: revisar reloj/pines");
    return;
  }

  // FFT -> barras
  static float bars[N_BARS];
  computeSpectrum(bars);

  // Peak hold con decaimiento suave
  const float decay = 0.02f; // descenso por frame
  for (uint8_t i = 0; i < N_BARS; i++) {
    if (bars[i] > peakHold[i]) peakHold[i] = bars[i];
    else peakHold[i] = max(0.0f, peakHold[i] - decay);
  }

  // Renderizado
  renderBars(bars, peakHold);
}

Resumen de bloques clave:
– Configuración de pines: coincide con la tabla de conexión.
– I2S: entrada a 16 kHz, 32 bits (24 válidos en INMP441), lectura como int32 y normalización a [-1, 1].
– FFT: N=1024, ventana Hann aplicada antes de la FFT, conversión a magnitud, agrupación logarítmica de bins.
– Pantalla: 64 barras, colores por altura, pico retenido (peak hold) con decaimiento.
– Control de FPS: 30 fps aproximados para fluidez.

Opcional: archivo de configuración de proyecto (estructura de carpetas)

Si organizas el sketch en una carpeta de proyecto, una estructura mínima:

i2s-spectrum-visualizer/
├─ i2s-spectrum-visualizer.ino
└─ README.md

Compilación, flash y ejecución

Usaremos Arduino CLI (0.35.3) con el core de Earle Philhower para RP2040 (4.1.2). Esto nos da una implementación I2S estable basada en PIO para el RP2040.

1) Instalar Arduino CLI

  • Linux/macOS:
  • Descarga binario desde https://arduino.github.io/arduino-cli/latest/installation/
  • Verifica versión:
    arduino-cli version
    Salida esperada: arduino-cli Version: 0.35.3

2) Agregar índice del core RP2040 (Earle Philhower) y actualizar

arduino-cli config init
arduino-cli config add board_manager.additional_urls https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
arduino-cli core update-index

3) Instalar el core RP2040 y bibliotecas

arduino-cli core install rp2040:rp2040@4.1.2
arduino-cli lib install "Adafruit GFX Library@1.11.9"
arduino-cli lib install "Adafruit ILI9341@1.6.1"
arduino-cli lib install "arduinoFFT@1.6.0"

Verificar:

arduino-cli core list
arduino-cli lib list | grep -E "Adafruit GFX|ILI9341|arduinoFFT"

4) Compilar el sketch

Asumiendo que estás dentro de la carpeta del proyecto y el archivo se llama i2s-spectrum-visualizer.ino:

arduino-cli compile \
  --fqbn rp2040:rp2040:nanorp2040connect \
  --build-property compiler.cpp.extra_flags="-O2" \
  .

Notas:
– FQBN exacto: rp2040:rp2040:nanorp2040connect (placa: Arduino Nano RP2040 Connect).
– Si quieres generar binarios sin cargar:
arduino-cli compile --fqbn rp2040:rp2040:nanorp2040connect --output-dir ./build .

5) Modo carga (UF2) y subida

El core de Earle permite carga automática vía puerto serie. Si falla, usa doble pulsación de reset para entrar en modo UF2 (aparece unidad RPI-RP2).

  • Subida directa (auto-reset) especificando puerto:
  • Linux/macOS:
    arduino-cli upload -p /dev/ttyACM0 --fqbn rp2040:rp2040:nanorp2040connect .
  • Windows (ejemplo):
    arduino-cli upload -p COM4 --fqbn rp2040:rp2040:nanorp2040connect .

  • Subida manual en modo UF2:

  • Pulsa dos veces el botón RESET rápido; aparecerá una unidad RPI-RP2.
  • Compila con:
    arduino-cli compile --fqbn rp2040:rp2040:nanorp2040connect --output-dir ./build .
  • Copia el UF2 resultante a la unidad:
    • Linux/macOS:
      cp ./build/i2s-spectrum-visualizer.ino.uf2 /media/$USER/RPI-RP2/
    • Windows:
      Copia el archivo .uf2 al volumen RPI-RP2 desde el Explorador.

6) Ejecución

  • Al reiniciar, la pantalla ILI9341 debe encender la retroiluminación y mostrarse el grid.
  • Emite algún sonido (palmada, música) cerca del INMP441 para verificar el espectro.

Validación paso a paso

1) Alimentación y pantalla:
– La pantalla debe iluminarse (LED/BLED). Si no:
– Revisa que LED/BLED esté a 3.3V con resistencia.
– Verifica VCC=3.3V y GND.

2) Inicialización:
– Debes ver el texto “i2s-spectrum-visualizer” en la esquina superior.
– Sin señal de audio, las barras deben estar bajas o cercanas a cero con ligeras fluctuaciones por ruido.

3) Captura I2S:
– Emite un tono a 1 kHz (por ejemplo, desde un generador de señal en el móvil).
– Observa una barra alta alrededor de la frecuencia correspondiente (cercana al 1 kHz).
– Si aparece “I2S timeout: revisar reloj/pines” en la pantalla, hay un problema con pines o reloj.

4) Respuesta en frecuencia:
– Usa tonos de 100 Hz, 500 Hz, 1 kHz, 2 kHz, 4 kHz:
– Verifica que la barra dominante se desplace hacia la derecha a medida que sube la frecuencia.
– A frecuencias > 8 kHz no debería mostrarse actividad (límite de Nyquist a Fs=16 kHz).

5) Dinámica:
– Aumenta y disminuye el volumen de la fuente.
– Las barras deben crecer/disminuir y los picos (líneas cian) deben mantenerse brevemente y decaer suavemente.

6) Estabilidad temporal:
– Observa por 1–2 minutos:
– Debe mantener 25–30 fps aproximadamente, sin parpadeos notables.
– La CPU del RP2040 debe ser suficiente; si notas caídas, reduce N_BARS o N_SAMPLES.

7) Depuración por consola (opcional):
– Abre el monitor serie a 115200 baudios:
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200
– Puedes imprimir valores intermedios descomentando logs en el código para confirmar magnitudes.

Troubleshooting

1) Pantalla en negro
– Causas probables:
– LED/BLED no conectado a 3.3 V con resistencia.
– RST, CS o DC mal cableados.
– Alimentación insuficiente en 3.3V (conexiones flojas).
– Solución:
– Verifica conexiones con la tabla.
– Prueba bajando la velocidad SPI (añadir tft.setSPISpeed(16000000) si fuese necesario) o usa cables más cortos.

2) Mensaje “Error I2S.begin()”
– Causas:
– API de I2S en versión del core diferente a la esperada.
– Pines BCLK/LRCLK/SD ocupados o mapeados incorrectamente.
– Soluciones:
– Asegura rp2040:rp2040@4.1.2. Si usas otra versión, adapta a:
i2s.begin(I2S_PHILIPS_MODE, SAMPLE_RATE, 32);
– Cambia pines I2S a otros GPIO compatibles (p. ej., D14–D16) y actualiza el código.

3) “I2S timeout: revisar reloj/pines”
– Causas:
– INMP441 sin alimentación o con GND flotante.
– Cableado de SD/BCLK/LRCLK inverso.
– L/R del INMP441 en estado indeterminado.
– Soluciones:
– Confirma VDD=3.3V, GND común.
– Revisa SCK->D3, WS->D2, SD->D4.
– Conecta L/R a GND (o a 3.3V si deseas canal derecho).

4) Barras no corresponden a la frecuencia (pico desplazado)
– Causas:
– Frecuencia de muestreo efectiva distinta (Fs no coincide).
– Formato de dato del INMP441 (desplazamiento de 24/32 bits).
– Soluciones:
– Verifica SAMPLE_RATE=16000 en código y que i2s.begin lo acepte.
– Ajusta la normalización: si el audio está “bajo”, prueba sample/ (float)(1<<31) o sample >> 8 antes de normalizar.

5) Rendimiento bajo o parpadeo
– Causas:
– N_SAMPLES alto + N_BARS alto.
– Redibujado completo en cada frame.
– Soluciones:
– Reduce N_BARS a 48 o 32.
– Baja SAMPLE_RATE a 12000 Hz si tu uso lo permite.
– Optimiza renderizado: dibuja solo barras que cambian.

6) Artefactos o ruido excesivo
– Causas:
– Cables I2S largos o cercanos a líneas SPI.
– Masa ruidosa.
– Soluciones:
– Cableado corto y trenzado (BCLK+GND, LRCLK+GND).
– Añadir condensador cerámico 0.1 µF cerca del INMP441 entre VDD y GND.

7) No se detecta el puerto en Windows
– Causas:
– Driver CDC-ACM no instalado correctamente.
– Cable USB solo carga.
– Soluciones:
– Probar otro puerto USB y cable “de datos”.
– Actualizar Windows; reinstalar el core desde Arduino IDE para forzar drivers.

8) Error al subir por arduino-cli (auto-reset)
– Causas:
– Bootloader no entra en modo programación.
– Soluciones:
– Doble pulsación de RESET para entrar a RPI-RP2 y copiar el .uf2 manualmente.
– Asegurar permisos en Linux (regla udev para /dev/ttyACM*).

Mejoras y variantes

1) Escala logarítmica más precisa
– Reemplazar el mapeo de bins por bandas tipo tercio de octava con pesos, para visualización más musical.

2) Aumento de resolución temporal o espectral
– N_SAMPLES=2048 a costa de FPS.
– Uso de buffers de doble canal DMA si cambias a 48 kHz y reduces N_BARS.

3) Colores dinámicos y temas
– Gradientes HSV basados en frecuencia y amplitud.
– Picos con “cola” y gravedad variable.

4) Compresor/AGC
– Implementar AGC simple para mantener la visualización estable ante cambios de nivel.

5) Curva psicoacústica (A-weighting)
– Aplicar ponderación A o curvas custom para enfatizar bandas de interés.

6) Modo waterfall
– Mantener un historial y desplazar la pantalla para formar un espectrograma 2D.

7) Control táctil o botones
– Si tu módulo ILI9341 es táctil (XPT2046), cambiar parámetros (N_BARS, escala) en tiempo real.

8) Conectividad
– El Nano RP2040 Connect incorpora WiFi/BLE vía módulo NINA. Publicar niveles de banda vía UDP o BLE para monitor remoto (no cubierto aquí).

Checklist de verificación

  • [ ] He instalado Arduino CLI 0.35.3 y verificado su versión.
  • [ ] He añadido la URL de Earle Philhower y he instalado rp2040:rp2040@4.1.2.
  • [ ] He instalado las librerías: Adafruit GFX 1.11.9, Adafruit ILI9341 1.6.1, arduinoFFT 1.6.0.
  • [ ] He cableado INMP441: VDD->3.3V, GND->GND, SCK->D3, WS->D2, SD->D4, L/R->GND.
  • [ ] He cableado ILI9341: VCC->3.3V, GND->GND, CS->D10, DC->D9, RST->D8, MOSI->D11, SCK->D13, LED->3.3V con resistencia.
  • [ ] He compilado con: arduino-cli compile –fqbn rp2040:rp2040:nanorp2040connect .
  • [ ] He subido el firmware: arduino-cli upload -p –fqbn rp2040:rp2040:nanorp2040connect .
  • [ ] La pantalla muestra “i2s-spectrum-visualizer” y el grid inicial.
  • [ ] Al emitir tonos (100 Hz a 4 kHz), veo barras dominantes coherentes.
  • [ ] Los picos cian retienen el máximo y decaen suave.
  • [ ] No hay errores de “I2S timeout” durante la operación normal.

Apéndice: comandos resumidos

# 1) Inicializar CLI y añadir core RP2040
arduino-cli config init
arduino-cli config add board_manager.additional_urls https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
arduino-cli core update-index
arduino-cli core install rp2040:rp2040@4.1.2

# 2) Instalar librerías
arduino-cli lib install "Adafruit GFX Library@1.11.9"
arduino-cli lib install "Adafruit ILI9341@1.6.1"
arduino-cli lib install "arduinoFFT@1.6.0"

# 3) Compilar y subir
arduino-cli compile --fqbn rp2040:rp2040:nanorp2040connect .
arduino-cli upload -p /dev/ttyACM0 --fqbn rp2040:rp2040:nanorp2040connect .

Comentarios finales

Este caso práctico integra adquisición I2S a nivel de hardware (RP2040 + PIO mediante la librería I2S del core Earle Philhower), procesamiento en el dominio de la frecuencia con FFT, y visualización en tiempo real sobre un controlador gráfico SPI. El conjunto “Arduino Nano RP2040 Connect + INMP441 + ILI9341” es una base sólida para aplicaciones de análisis de audio embebido, diagnóstico de ruido y visualizaciones musicales, manteniendo un flujo de trabajo reproducible gracias a versiones de toolchain bloqueadas y un pipeline de compilación/carga 100% por línea de comandos.

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é sistema operativo no se menciona como probado para el uso del Arduino Nano RP2040 Connect?




Pregunta 2: ¿Cuál es la versión de Arduino CLI mencionada en el artículo?




Pregunta 3: ¿Qué biblioteca se incluye en el core rp2040:rp2040 4.1.2?




Pregunta 4: ¿Qué puerto serie se utiliza típicamente en Linux para el Arduino Nano RP2040 Connect?




Pregunta 5: ¿Qué tipo de alimentación requieren todos los periféricos mencionados?




Pregunta 6: ¿Qué resistencia se sugiere para el pin LED/BLED de la pantalla?




Pregunta 7: ¿Qué controlador se utiliza en la pantalla TFT ILI9341?




Pregunta 8: ¿Qué cable se necesita para conectar el Arduino Nano RP2040 Connect?




Pregunta 9: ¿Qué modelo de micrófono se menciona en el artículo?




Pregunta 10: ¿Qué versión del core para RP2040 se menciona?




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:


Practical case: Real-time I2S audio spectrum analyzer

Practical case: Real-time I2S audio spectrum analyzer — hero

Objective and use case

What you’ll build: A real-time audio spectrum visualizer that captures audio via an INMP441 I2S microphone and renders FFT bars on an ILI9341 TFT display using the Arduino Nano RP2040 Connect.

Why it matters / Use cases

  • Visualize audio frequencies in real-time for music analysis and sound engineering.
  • Implement a portable audio monitoring tool for live performances using the Arduino Nano RP2040 Connect.
  • Create an educational tool for teaching FFT concepts and digital signal processing in embedded systems.
  • Develop a prototype for IoT applications that require audio analysis and data visualization.

Expected outcome

  • Real-time rendering of audio spectrum with a refresh rate of 30 FPS.
  • Accurate frequency representation with a latency of less than 50ms from audio capture to display.
  • Ability to handle audio input from the INMP441 at sample rates up to 48kHz.
  • Clear visualization of at least 32 frequency bands on the ILI9341 display.

Audience: Intermediate to advanced embedded systems developers; Level: Advanced

Architecture/flow: Audio input via INMP441 → I2S data processing → FFT computation → Visualization on ILI9341 display

Advanced Practical Case: I2S Spectrum Visualizer on “Arduino Nano RP2040 Connect + INMP441 + ILI9341”

Objective: Build a real-time audio spectrum visualizer that captures audio via an external INMP441 I2S microphone and renders FFT bars on an ILI9341 TFT display using the Arduino Nano RP2040 Connect. This guide focuses on I2S audio sampling, FFT computation, and real-time graphical rendering with tight control of pins, libraries, and toolchain.

This is an advanced, hands-on tutorial that includes wiring, code, build/flash commands, and rigorous validation techniques. It uses PlatformIO (CLI) for reproducible builds and dependency pinning.


Prerequisites

  • Comfortable with:
  • C++ for embedded systems and Arduino framework
  • SPI, I2S digital audio, and FFT fundamentals
  • PlatformIO CLI operations
  • OS:
  • Windows 10/11, macOS 12+, or Ubuntu 22.04 LTS (or similar)
  • USB data cable (USB Micro-B) for Arduino Nano RP2040 Connect
  • Internet access for dependency retrieval

Driver notes:
– Arduino Nano RP2040 Connect uses native USB CDC; no CP210x/CH34x drivers are required for official boards.
– Linux users should install PlatformIO udev rules:
– Run: pio system info to verify
– Install rules: pio system install udev-rules and replug the board
– macOS/Windows: no extra drivers typically required.

Why PlatformIO:
– The Nano RP2040 Connect is not an AVR/UNO; consistent advanced workflows benefit from PlatformIO’s environment management, dependency pinning, and reproducible builds for the RP2040 platform.


Materials (exact models)

  • 1x Arduino Nano RP2040 Connect (ABX00053)
  • 1x INMP441 I2S MEMS Microphone breakout (e.g., “INMP441 I2S Interface Microphone Module,” pins: VDD, GND, SCK/BCLK, WS/LRCLK, SD, L/R)
  • 1x ILI9341 2.4″ or 2.8″ TFT SPI display breakout (320×240), pins typically: VCC, GND, CS, DC, RST, MOSI, MISO, SCK, LED (backlight)
  • Jumper wires (female-male and female-female)
  • External 3.3V logic environment (the Nano RP2040 is 3.3V tolerant on IO)

Note on power:
– INMP441 requires 3.3V. The Nano RP2040’s 3V3 pin is the correct source. Do not power the INMP441 with 5V.


Setup/Connection

We’ll connect the INMP441 for I2S input and attach an ILI9341 TFT with SPI. Pin choices avoid overlap between I2S and SPI while keeping wiring practical.

Key choices for I2S on RP2040 (PIO-driven):
– BCLK (I2S bit clock) → D2
– LRCLK/WS (word select) → D3
– SD (data from mic to MCU) → D6
– L/R (channel select on INMP441) → GND (use “Left” channel)

Key SPI connections for ILI9341:
– SPI SCK → D13 (SCK)
– SPI MOSI → D11 (MOSI)
– SPI MISO → D12 (MISO) [Some ILI9341 boards don’t use MISO; still safe to wire]
– TFT CS → D10
– TFT DC → D9
– TFT RST → D4
– VCC, LED → 3.3V (many modules accept 5V VCC via onboard regulator/level-shifters; check your module. If unsure, use 3.3V)
– GND → GND

Grounding:
– All GNDs must be common: Arduino GND, INMP441 GND, and TFT GND.

Power budget:
– The Nano RP2040 Connect can power both modules from USB in most cases. If display backlight draws significant current, prefer the module’s onboard regulator and validate power draw (typ. ILI9341 breakout <100 mA).

Wiring Table

Function Arduino Nano RP2040 Pin INMP441 Pin ILI9341 Pin
3.3V Power 3V3 VDD VCC, LED
Ground GND GND GND
I2S BCLK D2 SCK/BCLK
I2S LRCLK/WS D3 WS/LRCLK
I2S Data (Mic → MCU) D6 SD
INMP441 Channel Sel. L/R → GND
SPI SCK D13 SCK
SPI MOSI D11 MOSI
SPI MISO D12 MISO
TFT Chip Select D10 CS
TFT Data/Command D9 DC
TFT Reset D4 RST

Notes:
– Confirm your ILI9341 breakout’s pin labels. Some boards label DC as “RS.”
– If your ILI9341 requires 5V on VCC but uses on-board level shifting, you can use 5V; however, Nano RP2040 IO is 3.3V—ensure the breakout is 3.3V logic compatible. Most Adafruit-style ILI9341 breakouts handle both.


Full Code

We will use:
– I2S (via pschatzmann/AudioTools for RP2040 PIO I2S input)
– arduinoFFT for FFT computation
– Adafruit_GFX + Adafruit_ILI9341 for rendering

Project structure (PlatformIO):
– platformio.ini
– src/main.cpp

platformio.ini

[env:nano_rp2040_connect]
platform = raspberrypi@^1.12.0
board = nano_rp2040_connect
framework = arduino
lib_deps =
  adafruit/Adafruit GFX Library@^1.11.9
  adafruit/Adafruit ILI9341@^1.5.14
  arduinoFFT@^1.6.0
  pschatzmann/AudioTools@^1.9.3
build_flags =
  -D PIO_FRAMEWORK_ARDUINO_ENABLE_EXCEPTIONS
monitor_speed = 115200

src/main.cpp

#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <arduinoFFT.h>
#include <AudioTools.h>

// -------------------- Pin Assignments --------------------
#define TFT_CS   10
#define TFT_DC   9
#define TFT_RST  4

#define I2S_BCLK 2   // INMP441 SCK/BCLK
#define I2S_LRCK 3   // INMP441 WS/LRCLK
#define I2S_SD   6   // INMP441 SD (data out from mic to MCU)

// -------------------- Display Setup ----------------------
Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST);

// -------------------- Audio & FFT Params -----------------
static const uint32_t SAMPLE_RATE = 44100; // Hz
static const uint16_t FFT_SIZE    = 512;   // power of two, 256..2048
static const uint8_t  BAR_COUNT   = 32;    // number of visual bars
static const float    SMOOTHING   = 0.65f; // 0..1 (higher = more smoothing)

// FFT arrays
double vReal[FFT_SIZE];
double vImag[FFT_SIZE];
arduinoFFT FFT(vReal, vImag, FFT_SIZE, SAMPLE_RATE);

// -------------------- I2S (AudioTools) -------------------
I2SStream i2s; // input stream (RP2040 PIO-based)
I2SConfig cfg;

// -------------------- Visualization State ----------------
float barHistory[BAR_COUNT]; // for smoothing

// Map k (bin index) to frequency
static inline double binToFreq(uint16_t k) {
  return (k * (double)SAMPLE_RATE) / (double)FFT_SIZE;
}

// Simple color map: low magnitude = blue, high = red/yellow
uint16_t colorMap(float norm) {
  // norm expected in [0,1]
  norm = constrain(norm, 0.0f, 1.0f);
  uint8_t r = (uint8_t)(255 * pow(norm, 1.5f));
  uint8_t g = (uint8_t)(255 * sqrtf(norm));
  uint8_t b = (uint8_t)(255 * (1.0f - norm));
  return tft.color565(r, g, b);
}

// Log-scale bin grouping from FFT bins to BAR_COUNT bars
void makeBarMagnitudes(float outBars[], const double magnitudes[]) {
  uint16_t kMin = 2; // skip DC/near-DC bins (0,1)
  uint16_t kMax = FFT_SIZE / 2; // Nyquist

  // Log-frequency mapping
  double fMin = 50.0;  // ignore ultra-low
  double fMax = 8000.0; // up to ~8kHz (speech/music visuals)
  for (uint8_t b = 0; b < BAR_COUNT; b++) {
    double frac = (double)b / (double)(BAR_COUNT - 1);
    double fLo = pow(10.0, log10(fMin) + frac * (log10(fMax) - log10(fMin)));
    double fHi = pow(10.0, log10(fMin) + (frac + (1.0 / BAR_COUNT)) * (log10(fMax) - log10(fMin)));
    if (fHi > fMax) fHi = fMax;

    // Accumulate bins within [fLo, fHi]
    double acc = 0.0;
    uint16_t count = 0;
    for (uint16_t k = kMin; k < kMax; k++) {
      double f = binToFreq(k);
      if (f >= fLo && f < fHi) {
        acc += magnitudes[k];
        count++;
      }
    }
    double mean = (count > 0) ? acc / (double)count : 0.0;

    // Convert to decibels for better dynamic range handling
    double db = 20.0 * log10(mean + 1e-9); // avoid log(0)
    // Normalize approx. -90..0 dB → 0..1
    float norm = (float)((db + 90.0) / 90.0);
    outBars[b] = constrain(norm, 0.0f, 1.0f);
  }
}

void setupI2S() {
  cfg = i2s.defaultConfig(RX_MODE);
  cfg.sample_rate    = SAMPLE_RATE;
  cfg.bits_per_sample= 32;        // INMP441 outputs 24-bit in 32-bit frames
  cfg.channels       = 1;         // Using left channel (L/R pin grounded)
  cfg.pin_bclk       = I2S_BCLK;
  cfg.pin_ws         = I2S_LRCK;
  cfg.pin_data       = I2S_SD;    // data from mic to MCU
  cfg.i2s_format     = I2S_STD_FORMAT; // Philips I2S standard
  cfg.use_apll       = false;     // not used on RP2040
  cfg.is_master      = true;      // RP2040 provides BCLK/LRCLK

  i2s.begin(cfg);
  // Warm-up: discard initial samples to let clocks stabilize
  int32_t dummy = 0;
  for (int i = 0; i < 4096; i++) {
    i2s.readBytes((uint8_t*)&dummy, sizeof(dummy));
  }
}

void setupTFT() {
  SPI.begin();
  tft.begin();
  tft.setRotation(1); // Landscape
  tft.fillScreen(ILI9341_BLACK);
  tft.setTextSize(1);
  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
  tft.setCursor(0, 0);
  tft.println("I2S Spectrum Visualizer (Nano RP2040 Connect)");
}

void drawBars(const float bars[BAR_COUNT]) {
  const int16_t W = tft.width();   // 320
  const int16_t H = tft.height();  // 240
  const int16_t margin = 8;
  const int16_t usableH = H - 2*margin - 20; // leave top line for text
  const int16_t usableW = W - 2*margin;
  const int16_t barGap = 2;
  const int16_t barW = (usableW - (BAR_COUNT - 1) * barGap) / BAR_COUNT;

  for (uint8_t i = 0; i < BAR_COUNT; i++) {
    // Exponential smoothing for stable bars
    float smoothed = SMOOTHING * barHistory[i] + (1.0f - SMOOTHING) * bars[i];
    barHistory[i] = smoothed;

    int16_t x = margin + i * (barW + barGap);
    int16_t h = (int16_t)(smoothed * usableH);
    int16_t y = H - margin - h;

    // Clear previous bar area by overdrawing in black
    tft.fillRect(x, margin + 20, barW, usableH, ILI9341_BLACK);

    // Draw new bar
    uint16_t col = colorMap(smoothed);
    tft.fillRect(x, y, barW, h, col);
  }
}

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println("Booting: I2S Spectrum Visualizer on Nano RP2040 Connect");

  setupTFT();
  setupI2S();

  memset(barHistory, 0, sizeof(barHistory));

  Serial.println("Init complete.");
}

void loop() {
  // 1) Acquire FFT_SIZE samples from I2S (32-bit frames)
  uint16_t i = 0;
  while (i < FFT_SIZE) {
    int32_t s32;
    size_t read = i2s.readBytes((uint8_t*)&s32, sizeof(s32));
    if (read == sizeof(s32)) {
      // Convert 24-bit left-justified to 16/24-bit int amplitude
      // INMP441: 24-bit valid, MSB aligned; right-shift by 8
      int32_t sample24 = (s32 >> 8);
      // Optional: downscale to 16-bit range for numerical stability
      int16_t s16 = (int16_t)(sample24 >> 8);

      vReal[i] = (double)s16;  // real signal
      vImag[i] = 0.0;          // imaginary = 0
      i++;
    } else {
      // no data yet, yield CPU briefly
      delayMicroseconds(50);
    }
  }

  // 2) Windowing + FFT
  FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
  FFT.Compute(FFT_FORWARD);
  FFT.ComplexToMagnitude();

  // 3) Aggregate bins to visualization bars
  float bars[BAR_COUNT];
  makeBarMagnitudes(bars, vReal);

  // 4) Render
  drawBars(bars);

  // Optional: print a quick diagnostic
  static uint32_t lastPrint = 0;
  uint32_t now = millis();
  if (now - lastPrint > 1000) {
    lastPrint = now;
    Serial.print("SR=");
    Serial.print(SAMPLE_RATE);
    Serial.print("Hz, FFT=");
    Serial.print(FFT_SIZE);
    Serial.print(", Bars=");
    Serial.println(BAR_COUNT);
  }
}

Build/Flash/Run Commands

These commands assume a clean workspace and PlatformIO installed.

1) Install/update PlatformIO CLI:

pio --version

2) Create project structure:

mkdir -p ~/work/i2s-spectrum-visualizer/src
cd ~/work/i2s-spectrum-visualizer

3) Initialize a PlatformIO project for Arduino Nano RP2040 Connect:

pio project init --board nano_rp2040_connect --project-option "framework=arduino"

4) Replace generated platformio.ini and create src/main.cpp as above:

# Create/overwrite platformio.ini
cat > platformio.ini <<'EOF'
[env:nano_rp2040_connect]
platform = raspberrypi@^1.12.0
board = nano_rp2040_connect
framework = arduino
lib_deps =
  adafruit/Adafruit GFX Library@^1.11.9
  adafruit/Adafruit ILI9341@^1.5.14
  arduinoFFT@^1.6.0
  pschatzmann/AudioTools@^1.9.3
build_flags =
  -D PIO_FRAMEWORK_ARDUINO_ENABLE_EXCEPTIONS
monitor_speed = 115200
EOF

# Add the C++ source
cat > src/main.cpp <<'EOF'
[PASTE THE FULL CODE FROM ABOVE HERE]
EOF

5) Build:

pio run -e nano_rp2040_connect

6) Upload firmware:

# Attempt auto-upload
pio run -e nano_rp2040_connect -t upload

If auto-upload doesn’t switch to UF2 bootloader:
– Double-tap the reset button on the Nano RP2040 Connect. A mass storage device named RPI-RP2 should appear.
– Manually copy the UF2:

# Linux example (adjust path to your mount point)
cp .pio/build/nano_rp2040_connect/firmware.uf2 /media/$USER/RPI-RP2/

7) Open serial monitor (for diagnostics):

pio device monitor -b 115200

Step-by-step Validation

1) Visual power-on check:
– The TFT should light up; you should see the title text in the top-left corner.
– If the screen is white only, check wiring for CS/DC/RST and SPI pins.

2) Serial diagnostics:
– Run pio device monitor -b 115200.
– You should see a line every second like: SR=44100Hz, FFT=512, Bars=32.
– If nothing appears, confirm the correct USB port is used or reset the board.

3) Audio capture baseline:
– With a quiet room, you should see low bars near the bottom.
– Gently rub your fingers or snap near the mic; bars should spike.

4) Frequency responsiveness:
– Whistle or play a pure tone (e.g., 1 kHz) using a phone app/signal generator.
– Observe which bar groups rise. Higher pitches should push energy toward bars on the right.

5) Sensitivity and dynamic range:
– If bars saturate easily, move the sound source further away.
– If bars are too weak, bring the source closer or increase the mic gain digitally (e.g., scale s16 before FFT), but avoid clipping.

6) Stability check:
– Speak or play music; bars should move smoothly without freezing or tearing.
– If flicker is excessive, increase SMOOTHING to 0.75–0.85.

7) Thermal/long-run:
– Let the system run for 10–15 minutes.
– Confirm it maintains stable rendering without gradual drift or lockups.

8) Reboot test:
– Press reset once. Firmware should boot directly (no double-tap needed) and resume operation.


Troubleshooting

  • No TFT output / white screen:
  • Verify SPI pins: SCK=D13, MOSI=D11, MISO=D12; check CS=D10, DC=D9, RST=D4.
  • Ensure common GND between TFT and Nano.
  • Some ILI9341 clones require lower SPI clock—Adafruit_ILI9341 defaults are fine, but you can slow SPI by wrapping SPISettings in advanced usage or lowering CPU load.

  • I2S data stuck (no movement in bars):

  • Check INMP441 power (use 3.3V) and common ground.
  • Confirm L/R is tied to GND so the mic outputs Left channel.
  • Ensure SCK/BCLK on D2 and WS/LRCLK on D3 are not swapped.
  • Shorter wires help (I2S is clocked at MHz-level rates; keep leads short and twisted where possible).
  • Reduce SAMPLE_RATE to 22050 for debugging.

  • Bars all zero or saturating:

  • If zero: Confirm data path. Print a few raw samples to Serial for inspection (temporarily).
  • If saturating: Check wiring for noise injection (separate display wires from mic lines), reduce proximity to high-noise sources, or reduce mic gain (digital scaling).

  • Upload failures:

  • Use manual UF2: double-tap reset to mount RPI-RP2, copy UF2.
  • Linux: ensure udev rules with pio system install udev-rules.
  • Try a different USB cable or port; avoid hubs during flashing.

  • Random lockups or flicker:

  • Try a lower FFT size (e.g., 256) to reduce CPU load.
  • Ensure good USB power; low-quality cables can brown out under display backlight load.
  • Use SMOOTHING >= 0.65 to reduce redraw turbulence.

  • Library conflicts:

  • Ensure versions match platformio.ini.
  • Run pio run -t clean then rebuild.

Improvements

  • Performance and fidelity:
  • Increase FFT_SIZE (1024) for finer frequency resolution; adjust bar aggregation accordingly. Note: higher FFT size means higher CPU and latency.
  • Use a Hann or Blackman-Harris window. Currently Hamming is configured; switch by changing FFT.Windowing type.
  • Implement double buffering and partial redraw to minimize flicker (draw only deltas).

  • Visual polish:

  • Add peak hold indicators per bar (decay over time).
  • Add color gradient per frequency band (blues for lows, reds for highs).
  • Show labels for frequency axis (e.g., 100 Hz, 1 kHz, 5 kHz) using small text.

  • Audio handling:

  • Implement AGC (automatic gain control) to maintain visible bars across varying input levels.
  • Use A-weighting or simple equalization to match human loudness perception.

  • Configuration menu:

  • Add a simple UI to switch FFT size, smoothing, and color scheme using buttons or serial commands.

  • Data logging:

  • Stream bar magnitudes over serial for analysis (CSV) and plot externally.

  • Power and thermal:

  • Dim the backlight via a transistor/PWM if your ILI9341 breakout exposes LED control and needs dimming.

  • Use onboard sensors:

  • The Nano RP2040 Connect includes an IMU; tilt-based UI for switching modes can be added.
  • It also has a built-in PDM microphone (not used in this project). You could implement a mode switch between INMP441 (I2S) and onboard PDM mic with compile-time flags.

Final Checklist

  • Materials:
  • Arduino Nano RP2040 Connect present and recognized over USB.
  • INMP441 powered at 3.3V; L/R tied to GND.
  • ILI9341 wired to SPI and control pins as specified.

  • Wiring integrity:

  • All grounds common.
  • I2S: BCLK→D2, LRCLK→D3, SD→D6. Short, tidy wires.
  • SPI: SCK→D13, MOSI→D11, MISO→D12, CS→D10, DC→D9, RST→D4.

  • Software:

  • PlatformIO installed and pio --version works.
  • platformio.ini matches provided content; library versions pinned.
  • Code placed at src/main.cpp without syntax alterations.

  • Build/flash:

  • pio run -e nano_rp2040_connect builds without errors.
  • Upload via pio run -e nano_rp2040_connect -t upload or manual UF2 copy.

  • Runtime validation:

  • Serial monitor at 115200 prints status.
  • TFT displays smooth, responsive spectrum bars that react to sound.
  • No persistent flicker, lock-ups, or power brownouts.

With this advanced setup, you’ve built a compact, real-time I2S spectrum visualizer that leverages the RP2040’s capability, PlatformIO’s reproducibility, and widely available display and audio modules.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary objective of the tutorial?




Question 2: Which microcontroller is used in this project?




Question 3: What type of microphone is used in the project?




Question 4: Which display type is utilized for rendering FFT bars?




Question 5: What programming language is primarily used in this tutorial?




Question 6: What is the purpose of PlatformIO in this project?




Question 7: Which operating systems are mentioned as prerequisites?




Question 8: What is required to connect the Arduino Nano RP2040 Connect?




Question 9: What do Linux users need to install for PlatformIO?




Question 10: Which command is used to verify the PlatformIO system information?




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

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:


Caso práctico: RFID y MQTT con Arduino Uno R4 WiFi y MFRC522

Caso práctico: RFID y MQTT con Arduino Uno R4 WiFi y MFRC522 — hero

Objetivo y caso de uso

Qué construirás: Un sistema avanzado de inventario RFID utilizando Arduino Uno R4 WiFi y el lector MFRC522, integrado con MQTT para la gestión de datos en tiempo real.

Para qué sirve

  • Gestión de inventario en tiempo real mediante la lectura de etiquetas RFID.
  • Notificaciones automáticas de cambios en el inventario a través de MQTT.
  • Control de acceso a áreas restringidas mediante identificación RFID.
  • Visualización de datos de inventario en aplicaciones web o móviles.

Resultado esperado

  • Lectura de hasta 100 etiquetas RFID por minuto.
  • Latencia de respuesta del sistema inferior a 200 ms al enviar datos al broker MQTT.
  • Actualización del estado del inventario en la interfaz de usuario en menos de 5 segundos.
  • Consumo de energía del sistema inferior a 200 mA durante la operación.

Público objetivo: Desarrolladores y técnicos en IoT; Nivel: Avanzado

Arquitectura/flujo: Arduino Uno R4 WiFi con MFRC522 conectado a la red WiFi, enviando datos a un broker MQTT y recibiendo actualizaciones para el control de LEDs WS2812B.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y entorno probado

  • Ubuntu 22.04.3 LTS (x86_64)
  • Alternativas compatibles:
  • Windows 11 23H2 (x64) con drivers USB nativos (WinUSB). No requiere drivers CH340/CP210x para Arduino Uno R4 WiFi.
  • macOS 13 Ventura o superior (Apple Silicon o Intel).

Toolchain exacta

  • Arduino CLI 0.35.3
  • Core/Paquete de placas: Arduino UNO R4 Boards (FQBN: arduino:renesas_uno:unor4wifi) versión 1.0.6
  • Compilador incluido en el core (arm-none-eabi-gcc provisto por el paquete arduino:renesas_uno 1.0.6)
  • Bibliotecas Arduino (versiones exactas):
  • WiFiS3 1.5.5
  • ArduinoMqttClient 0.1.8
  • MFRC522 1.4.11 (miguelbalboa/MFRC522)
  • Adafruit NeoPixel 1.12.0
  • ArduinoJson 6.21.3
  • SPI (incluida en el core)

Requisitos de red y broker

  • Red WiFi de 2.4 GHz con DHCP, SSID y contraseña conocidos.
  • Broker MQTT accesible (por ejemplo Mosquitto 2.x) en la red local sin TLS para simplificar la puesta en marcha inicial:
  • Host: 192.168.1.50 (ajusta a tu caso)
  • Puerto: 1883
  • Sin autenticación (o usuario/clave si tu broker lo exige; lo cubrimos más adelante).
  • Cliente de validación en PC:
  • mosquitto-clients (para mosquitto_pub y mosquitto_sub).

Materiales

  • 1 × Arduino Uno R4 WiFi (modelo exacto: UNO R4 WiFi — Renesas RA4M1 + co-procesador ESP32-S3 para conectividad).
  • 1 × Módulo RFID MFRC522 (versión con regulador a 3.3 V y adaptación de nivel incluida; a menudo etiquetado “RC522 SPI Module”).
  • 1 × Tira de LEDs WS2812B (8 a 16 LEDs es suficiente para el caso práctico).
  • 1 × Resistencia 330 Ω (en serie con la línea de datos de la tira WS2812B).
  • 1 × Condensador electrolítico 1000 µF / 6.3 V o superior (entre +5 V y GND de la tira LED, polarizado).
  • 1 × Fuente de 5 V externa con suficiente corriente para la tira LED (recomendado 2 A para margen).
  • Cables Dupont macho-hembra, protoboard opcional.
  • Tarjetas/llaveros RFID tipo MIFARE Classic (13.56 MHz) compatibles con MFRC522.

Nota sobre compatibilidad eléctrica:
– El MFRC522 es un dispositivo de 3.3 V. Se recomienda un módulo con regulador y adaptación de nivel integrada. Si tu módulo no tiene adaptación de nivel, usa un convertidor de nivel (bidireccional) para las líneas SPI desde el UNO R4 (5 V) hacia el MFRC522 (3.3 V). La línea MISO (3.3 V) suele ser aceptada por entradas de 5 V como “alto” lógico, pero mantén buenas prácticas y verifica tu módulo.

Preparación y conexión

Conexiones físicas (Arduino Uno R4 WiFi + MFRC522 + WS2812B)

Tabla de cableado:

Componente Señal/Pin en módulo Pin en Arduino Uno R4 WiFi Notas
MFRC522 SDA (SS) D10 Selección de esclavo SPI (SS)
MFRC522 SCK D13 SPI SCK (cabecera ICSP replicada)
MFRC522 MOSI D11 SPI MOSI
MFRC522 MISO D12 SPI MISO
MFRC522 RST D9 Reset del lector
MFRC522 3.3V 3.3V Alimentación del lector (no uses 5 V)
MFRC522 GND GND Referencia
WS2812B DIN D6 (a través de 330 Ω) Línea de datos con resistencia serie
WS2812B +5V 5V (fuente externa) Alimenta desde fuente externa de 5 V si >10 LEDs
WS2812B GND GND común Debe compartir GND con el Arduino
WS2812B Condensador 1000 µF entre +5V y GND Protege contra picos de arranque

Recomendaciones:
– Conecta primero GND común entre la fuente de 5 V, la tira WS2812B y el Arduino. Luego conecta +5 V a la tira. Finalmente la línea DIN con su resistencia de 330 Ω.
– Mantén cortos los cables de datos del WS2812B y del bus SPI.

Preparación del entorno de compilación (Arduino CLI)

1) Instala Arduino CLI 0.35.3 (Linux):
– Descarga desde https://github.com/arduino/arduino-cli/releases
– Copia el binario arduino-cli a /usr/local/bin y dale permisos de ejecución.

2) Inicializa Arduino CLI (si no lo usaste antes):
– arduino-cli config init

3) Actualiza índices e instala el core para UNO R4 WiFi:
– arduino-cli core update-index
– arduino-cli core install arduino:renesas_uno@1.0.6

4) Instala bibliotecas:
– arduino-cli lib install «WiFiS3@1.5.5»
– arduino-cli lib install «ArduinoMqttClient@0.1.8»
– arduino-cli lib install «MFRC522@1.4.11»
– arduino-cli lib install «Adafruit NeoPixel@1.12.0»
– arduino-cli lib install «ArduinoJson@6.21.3»

5) Verifica que el FQBN esté disponible:
– arduino-cli board listall | grep -i unor4wifi
– Debe listar: arduino:renesas_uno:unor4wifi

6) Permisos de puerto serie en Linux (si es necesario):
– sudo usermod -a -G dialout $USER
– Cierra sesión/inicia sesión.

Código completo

En este caso práctico implementaremos:
– Conexión WiFi (WiFiS3) y MQTT (ArduinoMqttClient).
– Lectura RFID (MFRC522 por SPI), detección de UIDs y “debounce”.
– Lógica de “inventory” simple: toggle check-in/check-out por UID.
– Publicación de eventos JSON en MQTT bajo un topic con el device_id derivado de la MAC.
– Feedback visual con WS2812B: estados de conexión, publicación y resultado de lectura.

Estructura de archivos recomendada:

  • rfid-inventory-mqtt/
  • rfid-inventory-mqtt.ino
  • secrets.h (no subir a repositorios públicos)
  • secrets.h.example (plantilla)

secrets.h.example (cópialo a secrets.h y rellena tus credenciales)

#pragma once

// WiFi
#define WIFI_SSID "TuSSID"
#define WIFI_PASS "TuPasswordWiFi"

// MQTT
#define MQTT_HOST "192.168.1.50"
#define MQTT_PORT 1883

// Si tu broker exige autenticación, rellena; si no, deja strings vacíos.
#define MQTT_USER ""
#define MQTT_PASSWD ""

rfid-inventory-mqtt.ino

#include <WiFiS3.h>
#include <ArduinoMqttClient.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Adafruit_NeoPixel.h>
#include <ArduinoJson.h>
#include "secrets.h"

// -------------------- Configuración de pines --------------------
constexpr uint8_t PIN_SS   = 10; // MFRC522 SDA/SS
constexpr uint8_t PIN_RST  = 9;  // MFRC522 RST
constexpr uint8_t PIN_PIX  = 6;  // WS2812B DIN
constexpr uint16_t NUM_PIX = 12; // Ajusta a tu tira (>=8 es suficiente)

// -------------------- Objetos globales -------------------------
MFRC522 rfid(PIN_SS, PIN_RST);
Adafruit_NeoPixel pixels(NUM_PIX, PIN_PIX, NEO_GRB + NEO_KHZ800);

// WiFi y MQTT
WiFiClient wifiClient;
MqttClient mqttClient(wifiClient);

// -------------------- Utilidades de LEDs -----------------------
void pixelsFill(uint8_t r, uint8_t g, uint8_t b, uint8_t brightness=50) {
  pixels.setBrightness(brightness);
  for (uint16_t i = 0; i < NUM_PIX; i++) {
    pixels.setPixelColor(i, pixels.Color(r,g,b));
  }
  pixels.show();
}

void pixelsFlash(uint8_t r, uint8_t g, uint8_t b, int times=2, int on_ms=120, int off_ms=60) {
  for (int i=0; i<times; i++) {
    pixelsFill(r,g,b,80);
    delay(on_ms);
    pixelsFill(0,0,0,0);
    delay(off_ms);
  }
}

// -------------------- Gestión de inventario --------------------
struct ItemEntry {
  String uid;
  bool present;
  uint32_t lastSeenMs;
};

constexpr size_t MAX_ITEMS = 64;
ItemEntry items[MAX_ITEMS];
size_t itemsCount = 0;

int findItemIndex(const String& uid) {
  for (size_t i=0; i<itemsCount; i++) {
    if (items[i].uid == uid) return (int)i;
  }
  return -1;
}

bool upsertItemToggle(const String& uid, uint32_t nowMs, bool& outPresent) {
  int idx = findItemIndex(uid);
  if (idx < 0) {
    if (itemsCount >= MAX_ITEMS) return false; // sin espacio
    items[itemsCount] = {uid, true, nowMs}; // primera vez => check-in
    outPresent = true;
    itemsCount++;
    return true;
  } else {
    // "Debounce": ignora si se vuelve a leer el mismo UID demasiado pronto
    if (nowMs - items[idx].lastSeenMs < 1500) {
      outPresent = items[idx].present;
      return false; // no se publica
    }
    items[idx].present = !items[idx].present;
    items[idx].lastSeenMs = nowMs;
    outPresent = items[idx].present;
    return true;
  }
}

// -------------------- WiFi/MQTT helpers ------------------------
String deviceId;
String baseTopic;

void deriveDeviceId() {
  byte mac[6];
  WiFi.macAddress(mac);
  char buf[32];
  snprintf(buf, sizeof(buf), "unor4wifi-%02X%02X%02X", mac[3], mac[4], mac[5]);
  deviceId = String(buf);
  baseTopic = "inventory/" + deviceId;
}

bool ensureWiFi() {
  if (WiFi.status() == WL_CONNECTED) return true;

  pixelsFill(0,0,60,60); // azul: intentando conexión WiFi
  Serial.print("Conectando a WiFi SSID=");
  Serial.println(WIFI_SSID);

  WiFi.disconnect();
  int status = WiFi.begin(WIFI_SSID, WIFI_PASS);
  unsigned long t0 = millis();
  while (WiFi.status() != WL_CONNECTED) {
    delay(250);
    Serial.print(".");
    if (millis() - t0 > 15000) {
      Serial.println("\nTimeout WiFi. Reintentando...");
      return false;
    }
  }

  Serial.println("\nWiFi conectado.");
  Serial.print("IP: "); Serial.println(WiFi.localIP());
  pixelsFlash(0, 80, 0, 2); // verde breve
  return true;
}

bool ensureMQTT() {
  if (mqttClient.connected()) return true;
  if (!ensureWiFi()) return false;

  pixelsFill(60,60,0,60); // amarillo: conectando MQTT
  mqttClient.setId(deviceId);
  mqttClient.setKeepAliveInterval(60);
  mqttClient.setCleanSession(true);

  Serial.print("Conectando a MQTT: ");
  Serial.print(MQTT_HOST); Serial.print(":"); Serial.println(MQTT_PORT);

  bool auth = (String(MQTT_USER).length() > 0);
  bool ok = false;
  if (auth) {
    ok = mqttClient.connect(MQTT_HOST, MQTT_PORT, MQTT_USER, MQTT_PASSWD);
  } else {
    ok = mqttClient.connect(MQTT_HOST, MQTT_PORT);
  }

  if (!ok) {
    Serial.print("Fallo MQTT, code=");
    Serial.println(mqttClient.connectError());
    pixelsFlash(80,0,60,2); // magenta: fallo
    return false;
  }

  // Publicamos LWT manual (opcional) o un online marker:
  String onlineTopic = baseTopic + "/status";
  mqttClient.beginMessage(onlineTopic);
  mqttClient.print("{\"status\":\"online\"}");
  mqttClient.endMessage();
  Serial.println("MQTT conectado.");

  pixelsFlash(0, 80, 0, 2); // verde breve
  return true;
}

String uidToHexString(MFRC522::Uid *uid) {
  char buf[3*10] = {0}; // hasta 10 bytes UID (normal 4/7)
  String s;
  for (byte i = 0; i < uid->size; i++) {
    snprintf(buf, sizeof(buf), "%02X", uid->uidByte[i]);
    s += buf;
    if (i < uid->size - 1) s += ":";
  }
  return s;
}

bool publishEvent(const String& uid, bool present) {
  if (!ensureMQTT()) return false;

  // Construimos el JSON con ArduinoJson (buffer fijo)
  StaticJsonDocument<256> doc;
  doc["device_id"] = deviceId;
  doc["event"] = present ? "checkin" : "checkout";
  doc["uid"] = uid;
  doc["ts_ms"] = millis();
  doc["rssi"] = WiFi.RSSI();

  String topic = baseTopic + "/events";
  String payload;
  serializeJson(doc, payload);

  Serial.print("Publicando en ");
  Serial.print(topic);
  Serial.print(": ");
  Serial.println(payload);

  pixelsFill(60, 40, 0, 70); // ámbar: publicando
  bool ok = mqttClient.beginMessage(topic);
  if (!ok) return false;
  mqttClient.print(payload);
  mqttClient.endMessage();
  pixelsFlash(0, 80, 0, 1); // verde: OK
  return true;
}

// -------------------- Setup y loop ------------------------------
void setup() {
  Serial.begin(115200);
  while (!Serial) { ; }

  pixels.begin();
  pixelsFill(0, 0, 0, 0);

  Serial.println("rfid-inventory-mqtt | UNO R4 WiFi + MFRC522 + WS2812B");

  // SPI + RFID
  SPI.begin();
  rfid.PCD_Init(PIN_SS, PIN_RST);
  byte v = rfid.PCD_ReadRegister(MFRC522::VersionReg);
  Serial.print("MFRC522 VersionReg: 0x"); Serial.println(v, HEX);

  // WiFi/MQTT
  if (!ensureWiFi()) {
    Serial.println("WiFi no disponible al inicio; se reintentará en loop.");
  }
  deriveDeviceId();
  ensureMQTT();

  // Indicador listo
  pixelsFlash(0, 0, 80, 2); // azul: listo
}

void loop() {
  // Mantener MQTT
  if (mqttClient.connected()) {
    mqttClient.poll();
  } else {
    // Reintentos espaciados
    static unsigned long lastTry = 0;
    if (millis() - lastTry > 5000) {
      ensureMQTT();
      lastTry = millis();
    }
  }

  // Lectura de tarjetas
  if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) {
    delay(10);
    return;
  }

  String uid = uidToHexString(&rfid.uid);
  uint32_t nowMs = millis();
  bool present = false;
  bool changed = upsertItemToggle(uid, nowMs, present);

  if (!changed) {
    // Evento ignorado por debounce; parpadeo corto azul
    pixelsFlash(0,0,60,1,60,40);
  } else {
    // Feedback: verde check-in, rojo check-out
    if (present) {
      pixelsFlash(0, 80, 0, 2, 100, 60);
    } else {
      pixelsFlash(80, 0, 0, 2, 100, 60);
    }

    if (!publishEvent(uid, present)) {
      Serial.println("Error publicando evento.");
      pixelsFlash(80,0,60,2); // magenta: error
    }
  }

  rfid.PICC_HaltA();
  rfid.PCD_StopCrypto1();
}

Breve explicación por bloques:
– Conectividad:
– ensureWiFi(): maneja la conexión WiFi y timeouts (15 s), con feedback en LEDs.
– ensureMQTT(): establece la sesión MQTT (keepalive 60 s, clean session), publica un mensaje de “online” y usa el topic base inventory/.
– RFID:
– Inicializa SPI y MFRC522; lee UID; convierte a string hex “AA:BB:CC:DD”.
– Aplica “debounce” de 1.5 s por UID para evitar múltiples eventos al mismo tag.
– Inventario:
– upsertItemToggle(): primera lectura de un UID ⇒ check-in; siguiente ⇒ check-out; se guarda un arreglo de hasta 64 entradas.
– LEDs:
– Azul: intentando conectar o listo; Verde: éxito; Rojo: check-out; Ámbar: publicación; Magenta: error.
– MQTT:
– Publica JSON con device_id, event, uid, ts_ms y rssi en inventory//events.

Compilación/flash/ejecución

Asumimos que estás en el directorio padre y vas a crear la carpeta del sketch.

1) Crea la estructura del proyecto:

mkdir -p ~/proyectos/rfid-inventory-mqtt
cd ~/proyectos/rfid-inventory-mqtt
# Crea los archivos
printf '%s\n' '#pragma once' \
'#define WIFI_SSID "TuSSID"' \
'#define WIFI_PASS "TuPasswordWiFi"' \
'#define MQTT_HOST "192.168.1.50"' \
'#define MQTT_PORT 1883' \
'#define MQTT_USER ""' \
'#define MQTT_PASSWD ""' > secrets.h
# (Luego edita secrets.h con tus credenciales reales)

2) Guarda el contenido de rfid-inventory-mqtt.ino en este directorio.

3) Instala core y librerías (si no lo hiciste antes):

arduino-cli core update-index
arduino-cli core install arduino:renesas_uno@1.0.6
arduino-cli lib install "WiFiS3@1.5.5"
arduino-cli lib install "ArduinoMqttClient@0.1.8"
arduino-cli lib install "MFRC522@1.4.11"
arduino-cli lib install "Adafruit NeoPixel@1.12.0"
arduino-cli lib install "ArduinoJson@6.21.3"

4) Conecta el Arduino Uno R4 WiFi por USB y localiza el puerto:

arduino-cli board list
# Ejemplo de salida:
# Port         Type              Board Name   FQBN
# /dev/ttyACM0 Serial Port (USB) Uno R4 WiFi  arduino:renesas_uno:unor4wifi

5) Compila:

arduino-cli compile --fqbn arduino:renesas_uno:unor4wifi ~/proyectos/rfid-inventory-mqtt

6) Sube el firmware:

arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:renesas_uno:unor4wifi ~/proyectos/rfid-inventory-mqtt

7) Abre el monitor serie (115200 baudios):

arduino-cli monitor -p /dev/ttyACM0 -c 115200

8) Instala cliente MQTT en tu PC (si no lo tienes):

# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y mosquitto-clients

Validación paso a paso

1) Verifica la secuencia de LEDs al encender:
– Azul fijo/parpadeo: intentando conectarse al WiFi/MQTT.
– Dos destellos verdes: conectado correctamente.

2) Verifica en el monitor serie (arduino-cli monitor):
– Debes ver algo como:
– rfid-inventory-mqtt | UNO R4 WiFi + MFRC522 + WS2812B
– MFRC522 VersionReg: 0x92 (u otro valor válido)
– Conectando a WiFi SSID=…
– WiFi conectado. IP: 192.168.1.123
– Conectando a MQTT: 192.168.1.50:1883
– MQTT conectado.

3) Suscríbete a los eventos desde tu PC:

mosquitto-sub -h 192.168.1.50 -t "inventory/unor4wifi-+/events" -v
# Alternativamente, tras ver el device_id exacto en el monitor, suscríbete a:
# mosquitto-sub -h 192.168.1.50 -t "inventory/unor4wifi-ABCD12/events" -v

4) Acerca una tarjeta/llavero RFID al MFRC522:
– Espera ver en consola serial la UID leída y “Publicando…” con el payload.
– En la suscripción MQTT, deberías ver algo como:
– inventory/unor4wifi-ABCD12/events {«device_id»:»unor4wifi-ABCD12″,»event»:»checkin»,»uid»:»DE:AD:BE:EF»,»ts_ms»:123456,»rssi»:-57}
– Un segundo toque con la misma tarjeta (pasados ~1.5 s) debe alternar “checkout”.

5) Verifica feedback de LEDs:
– Check-in: destellos verdes.
– Check-out: destellos rojos.
– Publicación: breve ámbar.
– Error de publicación: magenta.

6) Consulta el estado del dispositivo:

mosquitto_sub -h 192.168.1.50 -t "inventory/unor4wifi-+/status" -v
# Debe haber publicado {"status":"online"} tras conectar.

7) (Opcional) Prueba autenticación si tu broker lo requiere:

mosquitto_sub -h 192.168.1.50 -u tuuser -P tuclave -t "inventory/+/events" -v
# Asegúrate de haber rellenado MQTT_USER y MQTT_PASSWD en secrets.h y recompilado.

8) Estresa el sistema con varias UID y verifica la lógica:
– Pasa 3–5 tarjetas diferentes.
– Comprueba que cada una alterna entre check-in/check-out en cada lectura separada por >1.5 s.
– Observa que el array de items crece hasta MAX_ITEMS (64 por defecto).

Troubleshooting

1) El monitor serie no muestra nada / no ves el puerto:
– Verifica el cable USB (de datos, no solo carga).
– En Linux, añade tu usuario a dialout: sudo usermod -a -G dialout $USER y reinicia sesión.
– Ejecuta arduino-cli board list para confirmar /dev/ttyACM0.
– Prueba otro puerto USB. En Windows, revisa el Administrador de dispositivos (COMx).

2) Error al compilar: FQBN incorrecto o core no instalado:
– Asegúrate de usar exactamente: –fqbn arduino:renesas_uno:unor4wifi
– Instala el core: arduino-cli core install arduino:renesas_uno@1.0.6
– Repite arduino-cli core update-index si falla la descarga.

3) El MFRC522 no detecta tarjetas:
– Revisa que alimentas el módulo con 3.3 V (no 5 V).
– Comprueba el cableado SPI: SDA→D10, SCK→D13, MOSI→D11, MISO→D12, RST→D9 y GND común.
– Verifica que tu módulo posee adaptación de nivel o usa un level shifter para las señales desde el UNO R4 (5 V).
– Acerca la tarjeta a 2–3 cm. Observa el VersionReg: 0x91/0x92 suelen ser válidos; 0x00 o 0xFF indican wiring o alimentación incorrecta.

4) WS2812B parpadea errático o no enciende:
– Asegura GND común entre fuente de 5 V, Arduino y tira.
– Coloca la resistencia serie de 330 Ω en DIN y el condensador de 1000 µF entre +5 V y GND.
– Si la tira tiene más de 60 LEDs, alimenta por ambos extremos.
– Reduce el brillo (pixels.setBrightness) al probar.

5) No conecta a WiFi:
– Confirma SSID/clave en secrets.h.
– Asegura red 2.4 GHz habilitada (algunos AP mezclan 2.4/5 GHz con el mismo SSID; puede funcionar, pero confirma).
– Acerca el equipo al AP; revisa saturación de canal; verifica que el DHCP entrega IP.

6) No conecta a MQTT:
– Revisa que el broker esté accesible desde tu máquina (ping, telnet 1883).
– Si el broker requiere usuario/clave, rellena MQTT_USER/MQTT_PASSWD y recompila.
– En Mosquitto, revisa listeners y ACLs (listener 1883 0.0.0.0).
– Comprueba que no haya doble NAT o firewall bloqueando.

7) Publicación falla intermitente:
– Observa el RSSI en payload; si < -80 dBm, mejora la señal WiFi.
– Aumenta keepalive o reintentos en ensureMQTT(); reduce la frecuencia de publicaciones si hay mucha carga.

8) El sketch reinicia o se cuelga:
– Evita alimentar la tira WS2812B desde el 5 V del Arduino si supera ~8–10 LEDs; usa fuente externa.
– Revisa consumo total y calidad de la fuente 5 V (mínimo 1–2 A para 16 LEDs con brillo alto).
– Disminuye el tamaño de JSON si sospechas presión de memoria (ArduinoJson 256 B estático; ajusta si amplías campos).

Mejoras/variantes

  • Seguridad MQTT (TLS):
  • Usa un listener TLS en tu broker (8883) y certificados.
  • ArduinoMqttClient puede trabajar sobre WiFiSSLClient (siempre que WiFiS3 soporte TLS en tu versión). Implica gestionar certificados y memoria.

  • Persistencia de inventario:

  • Guarda el estado en la memoria flash (EEPROM emulada) en el UNO R4 tras cada cambio.
  • Carga el estado al inicio para no perderlo tras reinicio.

  • NTP/RTC y timestamps absolutos:

  • Sincroniza hora vía NTP y usa timestamps POSIX en “ts_ms” en lugar de millis(), aprovechando el RTC del UNO R4 WiFi.
  • Publica timezone y drift si necesitas auditoría precisa.

  • Comandos vía MQTT:

  • Suscríbete a inventory//cmd para:

    • reset: limpiar inventario.
    • status: publicar resumen de items presentes.
    • led:: feedback visual remoto.
  • Lote/batching:

  • Agrupa varios eventos en un JSON array si lees muchas tarjetas en ráfaga, para reducir overhead de MQTT.

  • QoS y Retained:

  • Ajusta QoS si tu broker/consumidor lo requiere.
  • Publica retained en status online/offline para facilitar descubrimiento.

  • Identificación de artículos:

  • Mantén un diccionario UID→SKU/Descripción en el consumidor MQTT (servidor) o descarga una tabla al dispositivo vía comandos.

Checklist de verificación

  • [ ] He instalado Arduino CLI 0.35.3 correctamente y puedo ejecutar arduino-cli version.
  • [ ] He instalado el core arduino:renesas_uno@1.0.6 y verifico el FQBN arduino:renesas_uno:unor4wifi.
  • [ ] He instalado las bibliotecas exactas: WiFiS3 1.5.5, ArduinoMqttClient 0.1.8, MFRC522 1.4.11, Adafruit NeoPixel 1.12.0, ArduinoJson 6.21.3.
  • [ ] He cableado el MFRC522 a D10/D13/D11/D12/D9 y lo alimento a 3.3 V; GND común.
  • [ ] He cableado la tira WS2812B al pin D6 con resistencia serie 330 Ω, condensador 1000 µF entre +5 V/GND, y fuente de 5 V adecuada con GND común.
  • [ ] He creado secrets.h con mis credenciales WiFi y parámetros del broker MQTT.
  • [ ] El sketch compila y se sube con: arduino-cli upload -p /dev/ttyACM0 –fqbn arduino:renesas_uno:unor4wifi.
  • [ ] El monitor serie muestra IP asignada y conexión MQTT exitosa.
  • [ ] Puedo suscribirme con mosquitto-sub a inventory/unor4wifi-+/events y recibo eventos JSON de check-in/checkout al acercar tarjetas.
  • [ ] Los LEDs muestran: verde al check-in, rojo al check-out, ámbar al publicar, magenta si hay error.

Con lo anterior, habrás implementado un flujo completo “rfid-inventory-mqtt” con el modelo exacto Arduino Uno R4 WiFi + MFRC522 + WS2812B, usando Arduino CLI, core y librerías en versiones concretas, y con una validación reproducible ponta a punta desde el hardware hasta el topic MQTT.

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 compatible mencionado para el entorno probado?




Pregunta 2: ¿Qué versión de Arduino CLI se requiere?




Pregunta 3: ¿Cuál es la versión del paquete de placas para Arduino UNO R4?




Pregunta 4: ¿Qué tipo de red WiFi se necesita para la configuración?




Pregunta 5: ¿Qué biblioteca se menciona para la conectividad MQTT?




Pregunta 6: ¿Qué componente se necesita para la lectura de RFID?




Pregunta 7: ¿Cuál es el voltaje de la fuente externa recomendada?




Pregunta 8: ¿Qué tipo de LEDs se utilizan en el proyecto?




Pregunta 9: ¿Qué tipo de broker MQTT se menciona como ejemplo?




Pregunta 10: ¿Qué puerto se utiliza para el broker MQTT en la configuración?




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:


Practical case: RFID+MQTT on Arduino Uno R4 WiFi+MFRC522

Practical case: RFID+MQTT on Arduino Uno R4 WiFi+MFRC522 — hero

Objective and use case

What you’ll build: An RFID-driven inventory system that reads tags with an MFRC522, indicates state on a WS2812B LED strip, and publishes inventory state and events to an MQTT broker from an Arduino Uno R4 WiFi using Arduino CLI.

Why it matters / Use cases

  • Track inventory in real-time by scanning RFID tags and updating the system instantly.
  • Utilize MQTT for efficient communication between the Arduino and a local broker, allowing for remote monitoring.
  • Provide visual feedback on inventory status through WS2812B LEDs, enhancing user interaction.
  • Implement a reliable solution for managing stock levels in retail or warehouse environments.
  • Enable automated alerts for low stock or inventory discrepancies via MQTT messages.

Expected outcome

  • Real-time inventory updates with less than 1 second latency between RFID scan and MQTT publish.
  • Successful retention of state messages for each RFID tag published to the MQTT broker.
  • Visual confirmation of Wi-Fi/MQTT connection status through LED color changes.
  • Ability to handle up to 100 packets/s for inventory updates without data loss.
  • Validation of end-to-end functionality using command-line tools with 100% success rate in test scenarios.

Audience: Advanced users; Level: Intermediate to advanced.

Architecture/flow: Arduino Uno R4 WiFi with MFRC522 reads RFID tags, processes data, and communicates with MQTT broker while providing feedback via WS2812B LEDs.

Hands-on Advanced Project: RFID Inventory over MQTT with Arduino Uno R4 WiFi + MFRC522 + WS2812B

Objective: Build an RFID-driven inventory system that reads tags with an MFRC522, indicates state on a WS2812B LED strip, and publishes inventory state and events to an MQTT broker from an Arduino Uno R4 WiFi using Arduino CLI (no GUI).

You will:
– Read and toggle inventory state for RFID tags.
– Publish retained state per tag and event messages to an MQTT broker.
– Use WS2812B LEDs as feedback for Wi-Fi/MQTT status and scan events.
– Validate end-to-end behavior with command-line tools.

This walkthrough is written for advanced users who want a precise, repeatable setup using CLI tooling, versioned libraries, and reliable wiring logic.


Prerequisites

  • Operating system:
  • Linux (Ubuntu/Debian), macOS, or Windows 10/11.
  • Arduino CLI installed and available in PATH:
  • Install instructions: https://arduino.github.io/arduino-cli/latest/installation/
  • USB data cable (USB-A to USB-C or USB-A to USB-B micro/mini depending on adapter; Uno R4 WiFi uses USB-C).
  • Local MQTT broker:
  • Option A: Install Mosquitto locally.
  • Option B: Run Mosquitto via Docker.
  • Basic familiarity with:
  • Terminal commands.
  • Arduino sketches and libraries.
  • MQTT topics and retained messages.

Driver notes:
– Arduino Uno R4 WiFi enumerates as a standard CDC/ACM serial device. On Windows, it appears under “Ports (COM & LPT)” with no extra driver required. On macOS/Linux it appears as /dev/tty.usbmodem/ttyACM.


Materials (exact model plus supporting parts)

Required:
– Microcontroller: Arduino Uno R4 WiFi (model name: “UNO R4 WiFi”)
– RFID reader: MFRC522 (breakout board “RFID-RC522 13.56 MHz” using MFRC522 IC)
– Addressable LEDs: WS2812B LED strip or stick (5 V, e.g., 8 LEDs)
– Logic-level shifter (4-channel, BSS138-based) for 5 V ↔ 3.3 V SPI signals to MFRC522
– 330 Ω resistor for WS2812B data line (recommended)
– 1000 µF electrolytic capacitor (≥6.3 V) across LED 5 V power rails (recommended for stability)
– Breadboard and jumper wires
– Optional: External 5 V supply if driving many LEDs (≥1 A for 30+ LEDs)

Notes:
– The MFRC522 board is a 3.3 V device. Many breakout boards do not have onboard level shifting. Always level-shift 5 V MCU outputs (SS/SDA, SCK, MOSI, RST) down to 3.3 V to avoid damaging the MFRC522. MISO from MFRC522 to Uno R4 WiFi can be connected directly (3.3 V → 5 V tolerant input).
– Uno R4 WiFi logic is 5 V. WS2812B operates at 5 V logic and power.


Setup/Connection

Follow the pin mapping in the table. Keep wires short for SPI and WS2812B data. Place the 1000 µF capacitor across the LED 5 V and GND near the strip. Insert a 330 Ω series resistor on the WS2812B data line.

Pin Mapping and Power

  • Use 3.3 V to power the MFRC522 module.
  • Level shift these MCU outputs down to 3.3 V: D10 (SS/SDA), D11 (MOSI), D13 (SCK), D9 (RST).
  • MISO (D12) can be direct (3.3 V output) into the Uno R4 WiFi.

Table: Connection summary

Function Arduino Uno R4 WiFi Pin Level-Shift? MFRC522 Pin WS2812B Notes
Power to MFRC522 3.3 V, GND N/A 3.3V, GND N/A Do not power MFRC522 from 5 V.
MFRC522 Reset D9 Yes (to 3.3) RST N/A Use BSS138 channel.
MFRC522 SS/SDA (select) D10 Yes (to 3.3) SDA(SS) N/A Use BSS138 channel.
SPI MOSI D11 Yes (to 3.3) MOSI N/A Use BSS138 channel.
SPI MISO D12 No (3.3 V→5V tolerant) MISO N/A Direct wire.
SPI SCK D13 Yes (to 3.3) SCK N/A Use BSS138 channel.
WS2812B Data D6 via 330 Ω No (5 V) DIN DIN Add 330 Ω in series close to LED input.
WS2812B Power 5 V, GND N/A 5V, GND 5V,GND Add 1000 µF cap across 5 V–GND near LEDs.

Additional guidance:
– Tie all grounds together (Uno GND, MFRC522 GND, WS2812B GND, and external 5 V ground if used).
– If powering many LEDs from an external 5 V supply, connect the grounds (common ground) and do not draw high LED current from the Uno’s 5 V pin.


Full Code (Arduino Sketch)

Save as rfid-inventory-mqtt/rfid-inventory-mqtt.ino

/*
  rfid-inventory-mqtt.ino
  Device: Arduino Uno R4 WiFi + MFRC522 + WS2812B
  Purpose: Toggle inventory presence per RFID tag and publish over MQTT with retained state.
  Feedback: WS2812B LED effects indicate Wi-Fi/MQTT status and scan results.

  Libraries:
    - WiFiS3
    - ArduinoMqttClient
    - MFRC522
    - Adafruit NeoPixel
*/

#include <SPI.h>
#include <MFRC522.h>
#include <WiFiS3.h>
#include <ArduinoMqttClient.h>
#include <Adafruit_NeoPixel.h>

// -------------------- User Config --------------------
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASSWORD";

// MQTT Broker (local LAN broker recommended for validation)
const char* MQTT_HOST = "192.168.1.100"; // change to your broker IP or hostname
const uint16_t MQTT_PORT = 1883;

// MQTT topic base
const char* TOPIC_BASE_STATES = "inventory/states/"; // retained per UID: present/absent
const char* TOPIC_EVENTS = "inventory/events";       // event stream: JSON payload

// LEDs: WS2812B
constexpr uint8_t LED_PIN = 6;
constexpr uint16_t LED_COUNT = 8; // adjust to match your strip/stick
constexpr uint8_t LED_BRIGHTNESS = 24; // be conservative

// MFRC522 pins (SPI HW pins are fixed: 11 MOSI, 12 MISO, 13 SCK)
constexpr uint8_t RFID_SS_PIN = 10;
constexpr uint8_t RFID_RST_PIN = 9;

// Debounce time to ignore rapid repeated scans of same tag (ms)
constexpr uint32_t SCAN_DEBOUNCE_MS = 1000;

// -------------------- Globals --------------------
MFRC522 mfrc522(RFID_SS_PIN, RFID_RST_PIN);

Adafruit_NeoPixel pixels(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

// Wi-Fi + MQTT
WiFiClient wifiClient;
MqttClient mqttClient(wifiClient);

// Derived clientId from MAC
char g_clientId[48] = {0};

// Simple inventory table
struct TagEntry {
  char uid[21];        // up to 20 hex chars + null (supports up to 10 bytes UID; MFRC522 typically 4 or 7)
  bool present;
  uint32_t lastSeenMs; // for debounce
};
constexpr size_t MAX_TAGS = 50;
TagEntry g_tags[MAX_TAGS];
size_t g_tagCount = 0;

// A tiny queue for messages to retry when MQTT is disconnected
struct Msg {
  char topic[64];
  char payload[128];
  bool retained;
};
constexpr size_t MAX_QUEUE = 10;
Msg g_queue[MAX_QUEUE];
size_t g_qHead = 0, g_qTail = 0;

// -------------------- Utility Functions --------------------
void ledSetAll(uint8_t r, uint8_t g, uint8_t b) {
  for (uint16_t i = 0; i < LED_COUNT; i++) {
    pixels.setPixelColor(i, pixels.Color(r, g, b));
  }
  pixels.show();
}

void ledFlash(uint8_t r, uint8_t g, uint8_t b, uint16_t onMs = 100, uint16_t offMs = 100, uint8_t times = 1) {
  for (uint8_t t = 0; t < times; t++) {
    ledSetAll(r, g, b);
    delay(onMs);
    ledSetAll(0, 0, 0);
    delay(offMs);
  }
}

void makeClientIdFromMac() {
  byte mac[6];
  if (WiFi.macAddress(mac) == 0) {
    snprintf(g_clientId, sizeof(g_clientId), "uno-r4wifi-%lu", (unsigned long)millis());
  } else {
    snprintf(g_clientId, sizeof(g_clientId), "uno-r4wifi-%02X%02X%02X%02X%02X%02X",
             mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  }
}

void uidToHex(const MFRC522::Uid& uid, char* outHex, size_t outLen) {
  // Convert up to uid.size bytes to hex string without separators
  size_t pos = 0;
  for (byte i = 0; i < uid.size && pos + 2 < outLen; i++) {
    pos += snprintf(outHex + pos, outLen - pos, "%02X", uid.uidByte[i]);
  }
  outHex[pos] = '\0';
}

int findTagIndex(const char* uidHex) {
  for (size_t i = 0; i < g_tagCount; i++) {
    if (strcmp(g_tags[i].uid, uidHex) == 0) return (int)i;
  }
  return -1;
}

bool recentlyScanned(const char* uidHex) {
  int idx = findTagIndex(uidHex);
  if (idx < 0) return false;
  return (millis() - g_tags[idx].lastSeenMs) < SCAN_DEBOUNCE_MS;
}

void setInventoryStateAndPublish(const char* uidHex, bool present) {
  // Update/insert in table
  int idx = findTagIndex(uidHex);
  if (idx < 0 && g_tagCount < MAX_TAGS) {
    idx = (int)g_tagCount++;
    strncpy(g_tags[idx].uid, uidHex, sizeof(g_tags[idx].uid) - 1);
    g_tags[idx].uid[sizeof(g_tags[idx].uid) - 1] = '\0';
    g_tags[idx].present = present;
  } else if (idx >= 0) {
    g_tags[idx].present = present;
  }
  if (idx >= 0) g_tags[idx].lastSeenMs = millis();

  // Topics
  char topicState[96];
  snprintf(topicState, sizeof(topicState), "%s%s", TOPIC_BASE_STATES, uidHex);

  // Payloads
  const char* stateStr = present ? "present" : "absent";

  // Publish retained state
  bool pubOk = false;
  if (mqttClient.connected()) {
    mqttClient.beginMessage(topicState, /*retained=*/true, /*qos=*/0);
    mqttClient.print(stateStr);
    pubOk = mqttClient.endMessage();
  }
  if (!pubOk) {
    // Queue it
    size_t next = (g_qTail + 1) % MAX_QUEUE;
    if (next != g_qHead) {
      strncpy(g_queue[g_qTail].topic, topicState, sizeof(g_queue[g_qTail].topic) - 1);
      snprintf(g_queue[g_qTail].payload, sizeof(g_queue[g_qTail].payload), "%s", stateStr);
      g_queue[g_qTail].retained = true;
      g_qTail = next;
    }
  }

  // Event JSON
  char evtJson[160];
  snprintf(evtJson, sizeof(evtJson),
           "{\"client\":\"%s\",\"uid\":\"%s\",\"present\":%s,\"ts_ms\":%lu}",
           g_clientId, uidHex, present ? "true" : "false", (unsigned long)millis());

  pubOk = false;
  if (mqttClient.connected()) {
    mqttClient.beginMessage(TOPIC_EVENTS, /*retained=*/false, /*qos=*/0);
    mqttClient.print(evtJson);
    pubOk = mqttClient.endMessage();
  }
  if (!pubOk) {
    // Queue event non-retained
    size_t next = (g_qTail + 1) % MAX_QUEUE;
    if (next != g_qHead) {
      strncpy(g_queue[g_qTail].topic, TOPIC_EVENTS, sizeof(g_queue[g_qTail].topic) - 1);
      strncpy(g_queue[g_qTail].payload, evtJson, sizeof(g_queue[g_qTail].payload) - 1);
      g_queue[g_qTail].retained = false;
      g_qTail = next;
    }
  }
}

void flushQueue() {
  while (mqttClient.connected() && g_qHead != g_qTail) {
    Msg& m = g_queue[g_qHead];
    mqttClient.beginMessage(m.topic, m.retained, 0);
    mqttClient.print(m.payload);
    if (mqttClient.endMessage()) {
      g_qHead = (g_qHead + 1) % MAX_QUEUE;
    } else {
      break; // stop trying this loop
    }
  }
}

// -------------------- Connectivity --------------------
void ensureWifi() {
  if (WiFi.status() == WL_CONNECTED) return;

  ledSetAll(0, 0, 0);
  Serial.print(F("Connecting to Wi-Fi SSID="));
  Serial.println(WIFI_SSID);

  WiFi.disconnect();
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED) {
    ledSetAll(0, 0, 10); delay(80);
    ledSetAll(0, 0, 0);  delay(120);
    if (millis() - start > 15000) {
      Serial.println(F("Wi-Fi connect timeout, retrying..."));
      start = millis();
      WiFi.disconnect();
      WiFi.begin(WIFI_SSID, WIFI_PASS);
    }
  }
  Serial.print(F("Wi-Fi connected, IP: "));
  Serial.println(WiFi.localIP());
  ledFlash(0, 16, 0, 100, 50, 2); // double green flash
}

void ensureMqtt() {
  if (mqttClient.connected()) return;

  Serial.print(F("Connecting to MQTT broker "));
  Serial.print(MQTT_HOST);
  Serial.print(F(":"));
  Serial.println(MQTT_PORT);

  mqttClient.setId(g_clientId);
  mqttClient.setKeepAliveInterval(30);

  // Connect loop with indicator
  uint8_t tries = 0;
  while (!mqttClient.connect(MQTT_HOST, MQTT_PORT)) {
    tries++;
    Serial.print(F("MQTT connect failed, code="));
    Serial.println(mqttClient.connectError());
    ledSetAll(16, 4, 0); delay(120);
    ledSetAll(0, 0, 0);  delay(180);
    if (tries >= 5) {
      // Recheck Wi-Fi then retry connect
      ensureWifi();
      tries = 0;
    }
  }

  Serial.println(F("MQTT connected"));
  ledFlash(0, 16, 0, 80, 40, 3); // triple green flash on MQTT OK

  // Announce presence (non-retained event)
  char hello[128];
  snprintf(hello, sizeof(hello), "{\"client\":\"%s\",\"event\":\"online\",\"ts_ms\":%lu}",
           g_clientId, (unsigned long)millis());
  mqttClient.beginMessage(TOPIC_EVENTS, false, 0);
  mqttClient.print(hello);
  mqttClient.endMessage();

  // Push any queued messages
  flushQueue();
}

// -------------------- Setup/Loop --------------------
void setup() {
  Serial.begin(115200);
  delay(120);

  pixels.begin();
  pixels.setBrightness(LED_BRIGHTNESS);
  ledSetAll(0, 0, 0);

  // Start SPI & RFID
  SPI.begin();
  mfrc522.PCD_Init(RFID_SS_PIN, RFID_RST_PIN);
  // Optionally tune antenna gain for better read range
  // mfrc522.PCD_SetAntennaGain(mfrc522.RxGain_max);

  // Wi-Fi + MQTT
  ensureWifi();
  makeClientIdFromMac();
  ensureMqtt();

  Serial.println(F("RFID Inventory MQTT ready."));
}

void loop() {
  // Maintain connectivity
  if (WiFi.status() != WL_CONNECTED) {
    ensureWifi();
  }
  if (!mqttClient.connected()) {
    ensureMqtt();
  }
  mqttClient.poll();

  // RFID read logic
  if (!mfrc522.PICC_IsNewCardPresent()) {
    delay(5);
    return;
  }
  if (!mfrc522.PICC_ReadCardSerial()) {
    delay(5);
    return;
  }

  // Convert UID to hex string
  char uidHex[21] = {0};
  uidToHex(mfrc522.uid, uidHex, sizeof(uidHex));
  Serial.print(F("Tag UID: "));
  Serial.println(uidHex);

  if (!recentlyScanned(uidHex)) {
    // Toggle presence
    int idx = findTagIndex(uidHex);
    bool newState = true;
    if (idx >= 0) {
      newState = !g_tags[idx].present; // toggle
    } else {
      newState = true; // first time -> present
    }
    setInventoryStateAndPublish(uidHex, newState);

    // Visual feedback
    if (newState) {
      ledFlash(0, 20, 0, 70, 50, 2); // present -> green flashes
    } else {
      ledFlash(20, 8, 0, 70, 50, 2); // absent -> amber flashes
    }
  } else {
    Serial.println(F("Duplicate scan ignored (debounced)."));
    ledFlash(0, 0, 16, 40, 40, 1);
  }

  // Clean up RFID state machine
  mfrc522.PICC_HaltA();
  mfrc522.PCD_StopCrypto1();

  // Periodically flush queued messages
  flushQueue();
}

What this sketch does:
– Connects to Wi-Fi and MQTT, retrying with LED feedback.
– Reads RFID tags from MFRC522; builds the UID hex string (uppercase, no separators).
– Maintains an in-memory inventory table of up to 50 tags.
– Toggle behavior: First scan sets present=true; next scan toggles to absent; and so on.
– Publishes:
– Retained per-tag state to inventory/states/ with payload present or absent.
– Non-retained event JSON to inventory/events with client ID, UID, boolean present, and ts_ms.
– LED feedback:
– Blue pulses when connecting Wi-Fi/MQTT, green flashes on successful connections, green/amber flashes for presence/absence changes, blue flash for ignored duplicate.


Build/Flash/Run Commands (Arduino CLI)

We will use Arduino CLI with the correct core and FQBN for Uno R4 WiFi: arduino:renesas_uno:unor4wifi

Commands below are cross-platform; replace the serial port accordingly.

mkdir -p ~/projects/rfid-inventory-mqtt
cd ~/projects/rfid-inventory-mqtt

# 2) Initialize Arduino CLI (first time) and update cores index
arduino-cli config init
arduino-cli core update-index

# 3) Install the Uno R4 core (Renesas)
arduino-cli core install arduino:renesas_uno

# 4) Install required libraries (latest versions)
arduino-cli lib install "WiFiS3"
arduino-cli lib install "ArduinoMqttClient"
arduino-cli lib install "MFRC522"
arduino-cli lib install "Adafruit NeoPixel"

# 5) Place the sketch file
#    Save the provided code as: rfid-inventory-mqtt/rfid-inventory-mqtt.ino
#    (Ensure the folder name matches the .ino filename.)

# 6) Identify your board's serial port
arduino-cli board list

# Example outputs:
# Port         Type              Board Name       FQBN
# /dev/ttyACM0 Serial Port (USB) Arduino Uno R4 WiFi arduino:renesas_uno:unor4wifi
# COM5         Serial Port (USB) Arduino Uno R4 WiFi arduino:renesas_uno:unor4wifi

# 7) Compile for Uno R4 WiFi
arduino-cli compile --fqbn arduino:renesas_uno:unor4wifi ~/projects/rfid-inventory-mqtt

# 8) Upload (replace port as detected)
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:renesas_uno:unor4wifi ~/projects/rfid-inventory-mqtt

# 9) Open serial monitor (115200 baud) for logs
arduino-cli monitor -p /dev/ttyACM0 -c 115200

Notes:
– If upload fails, double-press the Uno R4 WiFi’s reset button to enter the bootloader, then retry upload.
– On Windows, replace /dev/ttyACM0 with COMx (e.g., COM5).


Step-by-step Validation

We’ll validate end-to-end device, network, and MQTT behavior. Use physical tags (MIFARE Classic 1K/4K, NTAG213/215/216) compatible with MFRC522.

1) Prepare the MQTT broker

Option A: Local install (Debian/Ubuntu):

sudo apt-get update
sudo apt-get install -y mosquitto mosquitto-clients
sudo systemctl enable --now mosquitto
# Allow local test without auth (default on many distros for localhost).

Option B: Docker (cross-platform):

docker run -it --rm --name mosq -p 1883:1883 eclipse-mosquitto:2

If you need anonymous access for LAN, you can run a quick config (test purposes only):

# mosquitto.conf (test only!)
# listener 1883 0.0.0.0
# allow_anonymous true

2) Subscribe to topics for live observation

Open a terminal to subscribe:

# Show all inventory messages verbosely
mosquitto_sub -h 192.168.1.100 -p 1883 -t 'inventory/#' -v

Replace the broker IP/host to match your setup.

3) Power and monitor the device

  • Connect the Uno R4 WiFi via USB and open the Arduino CLI monitor:
  • arduino-cli monitor -p /dev/ttyACM0 -c 115200
  • Watch for:
  • Wi-Fi connect logs and IP shown.
  • MQTT connected and “RFID Inventory MQTT ready.” line.

LED indicators:
– On connecting: brief blue pulses.
– After MQTT connection: triple green flash.

4) Scan a tag (first time: present=true)

  • Approach a tag to the MFRC522 antenna.
  • CLI monitor output should show a Tag UID line.
  • The LEDs should flash green.
  • In the subscriber terminal, expect two messages:
  • inventory/states/ present
  • inventory/events {«client»:»uno-r4wifi-…»,»uid»:»«,»present»:true,»ts_ms»:…}

Also verify that the state topic is retained:
– Resubscribe (new terminal):
– mosquitto_sub -h 192.168.1.100 -p 1883 -t ‘inventory/states/#’ -v
– You should immediately see the last published retained state for that UID.

5) Scan the same tag again (toggle: present=false)

  • Scan the same UID a second time.
  • LEDs flash amber (absent).
  • Subscriber shows:
  • inventory/states/ absent (retained)
  • inventory/events {…,»present»:false,…}

6) Check duplicate debouncing

  • Rapidly tap the same tag again within 1 second.
  • The serial monitor shows “Duplicate scan ignored (debounced).”
  • LED shows a single blue flash.
  • MQTT should not receive new messages for that duplicate tap.

7) Test with two or more different tags

  • Repeat steps 4–6 with additional tags.
  • Observe distinct topics:
  • inventory/states/04A1B2C3
  • inventory/states/0BFFEED1
  • Each state topic is retained separately.

8) Power-cycle the Arduino and re-subscribe

  • Reset or unplug/replug the Uno R4 WiFi.
  • Once it reconnects, open a fresh subscriber:
  • mosquitto_sub -h 192.168.1.100 -t ‘inventory/states/#’ -v
  • You should see retained states from the broker without any scans.
  • Scan a tag; confirm state toggles and events publish correctly again.

Troubleshooting

Common issues and fixes:

  • No Wi-Fi connection:
  • Check SSID/password in code (case-sensitive).
  • Verify 2.4 GHz network; ESP32-S3-based radio (via WiFiS3) is 2.4 GHz only.
  • Ensure DHCP is enabled and the device obtains an IP address.
  • Try moving closer to the AP; reduce interference.

  • MQTT connect fails:

  • Confirm broker is reachable from your PC (telnet 192.168.1.100 1883).
  • Verify firewall rules to allow TCP/1883 on your broker host.
  • Confirm broker configuration allows connections (anonymous or username/password).
  • If using a hostname, ensure DNS resolves on your network; try using raw IP.

  • RFID not reading:

  • Power MFRC522 from 3.3 V pin, not 5 V.
  • Ensure level shifting on SS (D10), RST (D9), MOSI (D11), SCK (D13). MISO to D12 is direct.
  • Antenna orientation: place the card flat on the coil area; MFRC522 has limited range (a few cm).
  • Reduce potential interference: keep WS2812B data and power wires away from the reader antenna loop.
  • As a hardware test, you can compile and run the MFRC522 “DumpInfo” example quickly to confirm wiring:

    • arduino-cli compile –fqbn arduino:renesas_uno:unor4wifi –libraries «MFRC522»
    • arduino-cli upload -p /dev/ttyACM0 –fqbn arduino:renesas_uno:unor4wifi
  • WS2812B shows wrong colors or flickers:

  • Confirm you added a 330 Ω resistor in series on the data line near DIN.
  • Ensure a solid ground reference between Uno and LED strip.
  • Add the 1000 µF capacitor across 5 V and GND at the LED strip.
  • Lower brightness to reduce current spikes (LED_BRIGHTNESS).
  • Use a dedicated 5 V supply if driving many LEDs. Always common the grounds.

  • Serial port not detected:

  • Try a different USB cable (data-capable).
  • On Windows, check Device Manager for COM port changes.
  • Double-press reset to enter bootloader and then upload.

  • Memory/queue overflows:

  • If many scans occur while MQTT is disconnected, the small queue can overflow and drop events.
  • Consider reconnecting the broker or enlarging the queue (increase MAX_QUEUE) with caution.

Improvements

  • Security (TLS):
  • Use WiFiSSLClient with ArduinoMqttClient for MQTT over TLS (port 8883).
  • Broker must have a proper CA; memory constraints apply. Test with minimally sized CA and short topic names.
  • Authentication:
  • Configure broker to require username/password; use mqttClient.setUsernamePassword(«user»,»pass»);
  • Time synchronization:
  • Add NTP client to include real timestamps in events (ts_iso8601). Requires library and time sync on start.
  • Persistent inventory:
  • Persist state to non-volatile storage (e.g., emulated EEPROM or dedicated flash if supported) to preserve presence across reboots. Carefully manage write wear and atomics.
  • More expressive topics:
  • Publish to topics like inventory///states/ for multi-site deployments.
  • QoS tuning:
  • ArduinoMqttClient can publish with QoS 1 in many cases; adjust if your broker and library build supports it.
  • Health checks:
  • Publish periodic heartbeat to inventory/clients//status with online/offline retained last-will message.
  • Command/control:
  • Subscribe to inventory/cmd/ for remote reset, LED test, or rescan commands.

Final Checklist

  • Wiring:
  • MFRC522 powered at 3.3 V.
  • Level shifting used on D9/D10/D11/D13 to MFRC522.
  • MISO wired directly from MFRC522 to D12.
  • WS2812B DIN on D6 via 330 Ω; 1000 µF cap on 5 V rail near LEDs.
  • All grounds tied together.

  • Software:

  • Arduino CLI installed and core arduino:renesas_uno set up.
  • Libraries installed: WiFiS3, ArduinoMqttClient, MFRC522, Adafruit NeoPixel.
  • Sketch saved in rfid-inventory-mqtt/rfid-inventory-mqtt.ino.

  • Build and upload:

  • Compiled with: arduino:renesas_uno:unor4wifi FQBN.
  • Uploaded to correct serial port.
  • Serial monitor at 115200 baud shows connection logs.

  • MQTT validation:

  • Broker accessible at MQTT_HOST:MQTT_PORT.
  • Subscriber shows retained states per UID after scans.
  • Event JSON published on inventory/events on each toggle.

  • Behavior:

  • First scan: present=true (green flashes).
  • Second scan: present=false (amber flashes).
  • Rapid duplicate scans ignored for ~1 s (blue single flash).

If every item in this checklist is satisfied, your rfid-inventory-mqtt solution on Arduino Uno R4 WiFi is operational and ready for integration with dashboards, databases, or automation flows.


Helpful Commands (Reference)

Broker operations and subscriptions:

# Start mosquitto via Docker (ephemeral)
docker run -it --rm --name mosq -p 1883:1883 eclipse-mosquitto:2

# Subscribe to all inventory topics
mosquitto_sub -h 192.168.1.100 -t 'inventory/#' -v

# Inspect retained states only
mosquitto_sub -h 192.168.1.100 -t 'inventory/states/#' -v

# Publish a test event (broker/local testing)
mosquitto_pub -h 192.168.1.100 -t 'inventory/events' -m '{"client":"test","event":"hello"}'

With this setup, you can now integrate the rfid-inventory-mqtt stream into Node-RED, Home Assistant, Grafana, or your own backend, using retained per-tag state for reliable point-in-time inventory and events for real-time activity.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary objective of the project?




Question 2: Which microcontroller is used in this project?




Question 3: What type of LED strip is used for feedback in the project?




Question 4: What protocol is used to publish inventory state and events?




Question 5: Which operating systems are mentioned as prerequisites?




Question 6: What is required to connect the Arduino Uno R4 WiFi?




Question 7: What is the purpose of the Mosquitto broker in this project?




Question 8: What type of RFID reader is used in this project?




Question 9: What is required for validating end-to-end behavior?




Question 10: What is the installation link for Arduino CLI?




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

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:


Caso práctico: Nivel de agua LoRaWAN Arduino MKR WAN 1310

Caso práctico: Nivel de agua LoRaWAN Arduino MKR WAN 1310 — hero

Objetivo y caso de uso

Qué construirás: Un sistema de telemetría para monitorear el nivel de agua utilizando Arduino MKR WAN 1310, JSN-SR04T e INA219 a través de LoRaWAN.

Para qué sirve

  • Monitoreo remoto del nivel de agua en tanques para optimizar el riego agrícola.
  • Detección de inundaciones en áreas propensas mediante alertas en tiempo real.
  • Control del nivel de agua en sistemas de acuicultura para mantener la salud de los peces.
  • Integración con sistemas de gestión de recursos hídricos para análisis de datos históricos.

Resultado esperado

  • Actualizaciones de nivel de agua cada 5 minutos con latencia menor a 2 segundos.
  • Envío de datos a la nube con un máximo de 10 paquetes por hora.
  • Alertas de nivel crítico enviadas a través de MQTT con un tiempo de respuesta de menos de 1 segundo.
  • Consumo de energía promedio de 30 mA en modo de transmisión y 1 µA en modo de sueño.

Público objetivo: Ingenieros y desarrolladores de IoT; Nivel: Avanzado

Arquitectura/flujo: Sensores JSN-SR04T e INA219 conectados al Arduino MKR WAN 1310, transmitiendo datos a través de LoRaWAN a una plataforma en la nube.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y herramientas exactas

  • Sistemas operativos soportados:
  • Ubuntu 22.04 LTS (64‑bit) actualizado a 22.04.4
  • Windows 11 Pro 23H2 (64‑bit)
  • macOS 14.5 (Sonoma)

  • Toolchain (versión exacta):

  • Python 3.11.8
  • pipx 1.4.3
  • PlatformIO Core (CLI) 6.1.14
  • PlatformIO platform atmelsam 8.2.0
  • Arduino Core for SAMD (framework-arduino-samd) 1.8.13
  • GCC ARM Embedded (toolchain-gccarmnoneeabi) 1.90301.200702

  • Librerías Arduino (vía PlatformIO) con versiones exactas:

  • arduino-libraries/MKRWAN@1.3.1
  • adafruit/Adafruit INA219@1.2.1
  • greiman/NewPing@1.9.7
  • arduino-libraries/ArduinoLowPower@1.2.3

  • Drivers/puertos:

  • Arduino MKR WAN 1310 expone USB CDC (no requiere CP210x/CH34x).
  • Windows 10/11: driver CDC integrado.
  • Linux: configurar udev para /dev/ttyACM0 (ver más abajo).
  • macOS: no requiere drivers adicionales.

Requisitos de red LoRaWAN

  • Acceso a una red LoRaWAN (recomendado: The Things Stack (TTS) / The Things Network v3).
  • Identificadores OTAA:
  • JoinEUI/AppEUI (16 hex)
  • AppKey (32 hex)
  • Región/frecuencia (ejemplo: EU868 o US915)
  • Antena conectada a la MKR WAN 1310 antes del primer encendido.

Materiales

  • 1 × Arduino MKR WAN 1310 (modelo exacto).
  • 1 × Sensor ultrasónico impermeable JSN‑SR04T (versión de 4 pines).
  • 1 × Sensor de corriente/tensión INA219 (módulo con shunt de 0.1 Ω).
  • 1 × Convertidor DC‑DC step‑up 3.7 V → 5 V (mín. 1 A).
  • 1 × Batería LiPo 3.7 V (por ejemplo, 2000–5000 mAh) con conector JST para MKR.
  • 1 × Antena LoRa 868/915 MHz compatible con MKR WAN 1310.
  • Resistencias para divisor de nivel en la línea ECHO (5 V a 3.3 V):
  • 1 × 20 kΩ (R2 a GND)
  • 1 × 10 kΩ (R1 en serie con la señal ECHO hacia el pin de la MKR)
  • Cables dupont macho‑hembra.
  • Caja estanca IP65 (opcional, recomendado).
  • Abrazaderas y soporte para instalar el JSN‑SR04T en la parte superior del depósito.
  • PC con USB y cable micro‑USB.

Nota: Mantenemos la coherencia con el modelo “Arduino MKR WAN 1310 + JSN‑SR04T + INA219”. El step‑up 5 V y las resistencias son auxiliares para poder alimentar el JSN‑SR04T a 5 V y adaptar niveles de 5 V a 3.3 V (la MKR es 3.3 V).

Preparación y conexión

Consideraciones eléctricas y de señal

  • MKR WAN 1310 trabaja a 3.3 V en GPIO. JSN‑SR04T requiere 5 V de alimentación:
  • Trigger (TRIG) tolera 3.3 V como “alto” en la mayoría de unidades (funciona en campo).
  • Echo (ECHO) devuelve 5 V: imprescindibles resistencias para dividir a ~3.3 V.
  • INA219 alimentado a 3.3 V, pero puede medir la línea de 5 V del step‑up (bus hasta 26 V).
  • Recomendado: poner el INA219 “en serie” con la línea de 5 V que alimenta el JSN‑SR04T para telemetría de consumo del sensor (salud del sistema).

Tabla de cableado (puertos/pines)

Componente Pin/Terminal Arduino MKR WAN 1310 Notas
JSN‑SR04T VCC (5 V) Salida step‑up 5 V El step‑up toma la LiPo y genera 5 V
JSN‑SR04T GND GND GND común al step‑up y la MKR
JSN‑SR04T TRIG D6 3.3 V es suficiente como “alto”
JSN‑SR04T ECHO D7 (vía divisor) R1=10 kΩ en serie desde ECHO; R2=20 kΩ de D7 a GND (≈3.3 V en alto)
INA219 V+ Salida step‑up 5 V Entrada del bus a medir (hacia sensor)
INA219 V- VCC de JSN‑SR04T La salida del INA219 alimenta al sensor
INA219 GND GND común Común con MKR y step‑up
INA219 SDA SDA I2C (3.3 V)
INA219 SCL SCL I2C (3.3 V)
INA219 VCC 3V3 Alimentación lógica del INA219
Batería LiPo JST Conector BAT MKR gestiona carga/uso de LiPo
Step‑up 5 V (entrada) VIN/GND BAT/GND Alimentado por la LiPo de la MKR
Antena LoRa Conector u.FL/SMA MKR WAN 1310 Conectar antes de energizar
USB Micro‑USB MKR WAN 1310 Solo para programación y monitor serie

Notas de montaje:
– Mantén cables de TRIG/ECHO separados de la antena LoRa para evitar acoplamientos.
– El JSN‑SR04T debe colocarse rígido en la tapa del depósito, apuntando perpendicularmente al agua.
– Evita espuma/turbulencia en la vertical del sensor; si es inevitable, usa filtro de mediana y límite de tiempo.

Reglas de udev (Linux)

Crea el archivo /etc/udev/rules.d/99-arduino.rules con:

SUBSYSTEM=="tty", ATTRS{idVendor}=="2341", MODE:="0666", GROUP:="dialout"
KERNEL=="ttyACM*", MODE:="0666", GROUP:="dialout"

Luego:

sudo udevadm control --reload-rules
sudo udevadm trigger

Código completo

El proyecto consta de:
– src/main.cpp (código principal)
– include/secrets.h (claves OTAA y región)
– platformio.ini (configuración de PlatformIO, ver más adelante)

A continuación, el código completo de la aplicación y el archivo de secretos.

src/main.cpp

#include <Arduino.h>
#include <MKRWAN.h>
#include <Adafruit_INA219.h>
#include <ArduinoLowPower.h>
#include <NewPing.h>
#include "secrets.h"

// ------------------------ Configuración de pines y constantes ------------------------
#define PIN_TRIG 6
#define PIN_ECHO 7

// Distancia máxima relevante (en cm) para el depósito.
// El JSN-SR04T soporta hasta ~600 cm, ajusta según tu geometría.
#define MAX_DISTANCE_CM 600

// Intervalo de transmisión (segundos)
#ifndef TX_INTERVAL_SECONDS
#define TX_INTERVAL_SECONDS 300
#endif

// Calibración del depósito (distancias en milímetros)
#ifndef EMPTY_DISTANCE_MM
#define EMPTY_DISTANCE_MM 1500 // distancia sensor->agua cuando el tanque está vacío
#endif

#ifndef FULL_DISTANCE_MM
#define FULL_DISTANCE_MM 100   // distancia sensor->agua cuando el tanque está lleno (tope)
#endif

// Filtro de lecturas ultrasónicas
#define N_PINGS 9

// Estructuras y objetos globales
LoRaModem modem;
Adafruit_INA219 ina219; // dirección por defecto 0x40
NewPing sonar(PIN_TRIG, PIN_ECHO, MAX_DISTANCE_CM);

// ------------------------ Utilidades ------------------------

static uint16_t clamp_u16(int v) {
  if (v < 0) return 0;
  if (v > 65535) return 65535;
  return (uint16_t)v;
}

static int16_t clamp_i16(int v) {
  if (v < -32768) return -32768;
  if (v >  32767) return  32767;
  return (int16_t)v;
}

static float medianDistanceCm(uint8_t n) {
  // NewPing provee ping_median directamente
  unsigned int us = sonar.ping_median(n);
  if (us == 0) return NAN; // No eco
  // NewPing define conversiones: US_ROUNDTRIP_CM = 57 (aprox)
  float cm = us / US_ROUNDTRIP_CM;
  return cm;
}

static int16_t distanceMmFiltered() {
  float cm = medianDistanceCm(N_PINGS);
  if (isnan(cm) || cm <= 0.0f) return -1;
  int mm = (int)(cm * 10.0f + 0.5f); // redondeo a mm
  return clamp_i16(mm);
}

static int computeLevelPercent(int distance_mm) {
  // Convierte distancia a porcentaje de llenado en base a calibración
  // Más cerca (menor distancia) => mayor nivel.
  int span = EMPTY_DISTANCE_MM - FULL_DISTANCE_MM;
  if (span <= 0) return -1;
  int pct = (int)roundf((float)(EMPTY_DISTANCE_MM - distance_mm) * 100.0f / (float)span);
  if (pct < 0) pct = 0;
  if (pct > 100) pct = 100;
  return pct;
}

static void printDevEUI() {
  String devEui = modem.deviceEUI();
  Serial.print("DevEUI: ");
  Serial.println(devEui);
}

// ------------------------ LoRaWAN ------------------------

static bool loraBeginAndJoin() {
  // Inicializa el módem en la región especificada en secrets.h
  if (!modem.begin(LORAWAN_REGION)) {
    Serial.println("Error: no se pudo inicializar el módem LoRa.");
    return false;
  }

  // Opciones recomendadas
  modem.setADR(true);   // Adaptive Data Rate
  modem.dataRate(DEFAULT_DATARATE); // ver secrets.h
  modem.txPower(DEFAULT_TXPOWER_DBM);

  printDevEUI();

  // Claves OTAA
  String appEui = SECRET_APP_EUI;
  String appKey = SECRET_APP_KEY;

  Serial.print("Uniendo a la red (OTAA)...");
  int connected = modem.joinOTAA(appEui, appKey);
  if (!connected) {
    Serial.println(" fallo en join.");
    return false;
  }
  Serial.println(" OK");

  // Confirmar parámetros tras join
  Serial.print("DR actual: ");
  Serial.println(modem.dataRate());
  return true;
}

static bool loraSendUplink(const uint8_t* payload, size_t len, uint8_t fport = 2, bool confirmed = false) {
  modem.beginPacket();
  modem.setPort(fport);
  modem.write(payload, len);
  int err = modem.endPacket(confirmed);
  if (err > 0) {
    Serial.print("Uplink enviado (bytes=");
    Serial.print(len);
    Serial.println(").");
    return true;
  } else {
    Serial.print("Fallo uplink, err=");
    Serial.println(err);
    return false;
  }
}

// ------------------------ Setup y bucle ------------------------

void setup() {
  pinMode(PIN_TRIG, OUTPUT);
  pinMode(PIN_ECHO, INPUT); // Recuerda: hay un divisor resistivo hacia D7
  digitalWrite(PIN_TRIG, LOW);

  Serial.begin(115200);
  while (!Serial && millis() < 4000) {
    ; // Espera breve a monitor serie
  }
  Serial.println("\n[Inicio] lora-water-level-telemetry (MKR WAN 1310 + JSN-SR04T + INA219)");

  // INA219
  if (!ina219.begin()) {
    Serial.println("Error: INA219 no encontrado en I2C. Verifica cableado.");
  } else {
    // Calibración típica: 32V, 2A (depende de tu módulo/shunt)
    ina219.setCalibration_32V_2A();
    Serial.println("INA219 OK (calibrado 32V/2A).");
  }

  // LoRaWAN: iniciar y unirse
  if (!loraBeginAndJoin()) {
    Serial.println("No se pudo unir a LoRaWAN en setup. Se reintentará en el loop.");
  }
}

void loop() {
  // (Re)intento de join si no hay sesión
  if (!modem.connected()) {
    Serial.println("Reconectando a LoRaWAN...");
    if (!loraBeginAndJoin()) {
      Serial.println("Join fallido. Espera y reintento.");
      LowPower.sleep(15000);
      return;
    }
  }

  // 1) Medición ultrasónica
  int16_t d_mm = distanceMmFiltered();
  if (d_mm < 0) {
    Serial.println("Medición ultrasónica inválida (sin eco).");
  } else {
    Serial.print("Distancia: ");
    Serial.print(d_mm);
    Serial.println(" mm");
  }

  int level_pct = (d_mm > 0) ? computeLevelPercent(d_mm) : -1;
  if (level_pct >= 0) {
    Serial.print("Nivel estimado: ");
    Serial.print(level_pct);
    Serial.println(" %");
  }

  // 2) Telemetría de alimentación del sensor (en la línea 5V del step-up a través del INA219)
  float bus_v = ina219.getBusVoltage_V();       // Voltaje del bus ~5 V
  float shunt_v = ina219.getShuntVoltage_mV();  // mV en la resistencia shunt
  float cur_mA = ina219.getCurrent_mA();        // Corriente hacia el sensor
  float pwr_mW = ina219.getPower_mW();

  Serial.print("INA219: Vbus=");
  Serial.print(bus_v, 3);
  Serial.print(" V, Ishunt=");
  Serial.print(cur_mA, 1);
  Serial.print(" mA, P=");
  Serial.print(pwr_mW, 1);
  Serial.println(" mW");

  // 3) Empaquetado de payload (6 bytes)
  // Formato (puerto 2):
  // [0..1]: distancia_mm (uint16, big-endian)
  // [2..3]: vbus_mV (uint16, big-endian) = bus_v * 1000
  // [4..5]: current_mA * 10 (int16, big-endian), con saturación
  uint8_t payload[6];
  uint16_t dist_u16 = clamp_u16(d_mm);
  uint16_t vbus_mv = clamp_u16((int)(bus_v * 1000.0f + 0.5f));
  int16_t cur_mA_x10 = clamp_i16((int)(cur_mA * 10.0f));

  payload[0] = (uint8_t)((dist_u16 >> 8) & 0xFF);
  payload[1] = (uint8_t)(dist_u16 & 0xFF);
  payload[2] = (uint8_t)((vbus_mv >> 8) & 0xFF);
  payload[3] = (uint8_t)(vbus_mv & 0xFF);
  payload[4] = (uint8_t)((cur_mA_x10 >> 8) & 0xFF);
  payload[5] = (uint8_t)(cur_mA_x10 & 0xFF);

  // 4) Envío LoRaWAN
  bool ok = loraSendUplink(payload, sizeof(payload), 2 /*FPort*/, false /*unconfirmed*/);
  if (!ok) {
    Serial.println("Reintento de uplink en 10 s...");
    LowPower.sleep(10000);
  }

  // 5) Dormir para ahorrar energía
  // Nota: en SAMD21, sleep conserva estado, pero algunos esquemas de bajo consumo
  // requieren reconfigurar periféricos tras standby. Probado con ArduinoLowPower.
  uint32_t sleep_ms = (uint32_t)TX_INTERVAL_SECONDS * 1000UL;
  Serial.print("Sleep ");
  Serial.print(TX_INTERVAL_SECONDS);
  Serial.println(" s.");
  LowPower.sleep(sleep_ms);
}

include/secrets.h

#pragma once
// Región LoRaWAN: EU868, US915, AU915, AS923, KR920, IN865
#define LORAWAN_REGION EU868

// Data rate por defecto (depende de la región):
// EU868: 5 (SF7BW125), US915: 3 (SF7BW125), ajusta si es necesario
#define DEFAULT_DATARATE 5

// Potencia TX (dBm), sujeta a regulaciones de la región
#define DEFAULT_TXPOWER_DBM 14

// Claves OTAA (The Things Stack / TTN v3)
// Reemplaza con tus valores hexadecimales (en mayúsculas, sin espacios)
static const char SECRET_APP_EUI[] = "70B3D57ED0039BFF"; // JoinEUI/AppEUI
static const char SECRET_APP_KEY[] = "00112233445566778899AABBCCDDEEFF"; // AppKey

// Calibraciones del tanque (puedes sobreescribir por build_flags en PlatformIO)
#ifndef TX_INTERVAL_SECONDS
#define TX_INTERVAL_SECONDS 300
#endif

#ifndef EMPTY_DISTANCE_MM
#define EMPTY_DISTANCE_MM 1500
#endif

#ifndef FULL_DISTANCE_MM
#define FULL_DISTANCE_MM 100
#endif

Breve explicación de las partes clave:
– MKRWAN: abstrae el módulo Murata CMWX1ZZABZ de la MKR WAN 1310. begin(región), joinOTAA(), beginPacket()/endPacket().
– NewPing: gestiona tiempos de eco; ping_median(n) reduce outliers por espuma o salpicaduras.
– INA219: calibración 32V/2A es común en módulos con shunt de 0.1 Ω. Nos da corriente y voltaje de la línea 5 V del sensor.
– Empaquetado binario: payload compacto de 6 bytes, apto para LoRaWAN y sencillo de decodificar.
– LowPower.sleep(): standby del SAMD21 para reducir consumo entre mediciones.

Compilación/flash/ejecución

A continuación, el flujo completo con comandos exactos y ordenados usando PlatformIO CLI (versión 6.1.14). Se asume un directorio de trabajo vacío.

1) Instalar PlatformIO Core 6.1.14 (recomendado con pipx)
– Linux/macOS:

python3 --version
pipx --version
pipx install "platformio==6.1.14"
pio --version
  • Windows (PowerShell):
py -3.11 -m pip install --user pipx
py -3.11 -m pipx ensurepath
# Cierra y reabre PowerShell
pipx install "platformio==6.1.14"
pio --version

2) Inicializar proyecto para Arduino MKR WAN 1310

mkdir lora-water-level-telemetry
cd lora-water-level-telemetry
pio project init --board mkrwan1310 --project-option "platform=atmelsam@8.2.0" --project-option "framework=arduino"

3) Especificar versiones exactas de framework y librerías (platformio.ini)
Crea/edita platformio.ini con:

[env:mkrwan1310]
platform = atmelsam@8.2.0
framework = arduino
board = mkrwan1310
platform_packages =
  framework-arduino-samd@1.8.13
  toolchain-gccarmnoneeabi@1.90301.200702
lib_deps =
  arduino-libraries/MKRWAN@1.3.1
  adafruit/Adafruit INA219@1.2.1
  greiman/NewPing@1.9.7
  arduino-libraries/ArduinoLowPower@1.2.3
monitor_speed = 115200
build_flags =
  -DTX_INTERVAL_SECONDS=300
  -DEMPTY_DISTANCE_MM=1500
  -DFULL_DISTANCE_MM=100

4) Añadir el código fuente
– Crear include/secrets.h y src/main.cpp con el contenido mostrado arriba.

En Linux/macOS (ejemplo):

mkdir -p include src
$EDITOR include/secrets.h
$EDITOR src/main.cpp
$EDITOR platformio.ini

5) Compilar

pio run

6) Conectar la MKR WAN 1310 por USB
– Identificar el puerto:
– Linux: ls /dev/ttyACM
– macOS: ls /dev/tty.usbmodem

– Windows: revisar Administrador de Dispositivos (COMx)

7) Subir el firmware
– Linux/macOS:

pio run -t upload --upload-port /dev/ttyACM0
  • Windows (ejemplo COM6):
pio run -t upload --upload-port COM6

8) Monitor serie
– Linux/macOS:

pio device monitor -b 115200 --port /dev/ttyACM0
  • Windows:
pio device monitor -b 115200 --port COM6

Verás el DevEUI, el proceso de join y las lecturas/telemetrías periódicas.

Validación paso a paso

1) Preparación en The Things Stack (TTN v3):
– Crear Application (p. ej. app: lora-water-level-telemetry).
– Registrar un dispositivo OTAA:
– DevEUI: puedes leerlo desde el monitor serie (modem.deviceEUI()) y copiarlo al TTS.
– JoinEUI/AppEUI: define uno y consérvalo en secrets.h.
– AppKey: genera una clave segura y transpórtala a secrets.h.
– Seleccionar la región/frecuencia (ej. EU868). Debe coincidir con LORAWAN_REGION en secrets.h.

2) Primer arranque:
– Conectar antena LoRa a la MKR WAN 1310.
– Energizar la placa (USB + LiPo conectada).
– Abrir monitor serie: verificar salida similar a:
– “DevEUI: AABBCCDDEEFF1122”
– “Uniendo a la red (OTAA)… OK”
– Lecturas: “Distancia: 1234 mm”, “Nivel estimado: 30 %”
– “INA219: Vbus=5.002 V, Ishunt=37.5 mA, P=187.5 mW”
– “Uplink enviado (bytes=6). Sleep 300 s.”

3) Validación de uplink en TTS:
– En la consola, abre el dispositivo y la pestaña “Live data”/“Uplinks”.
– Debes ver un uplink cada TX_INTERVAL_SECONDS con port=2 y payload de 6 bytes.

4) Decodificador de payload (TTS > Payload formatter > Uplink > Javascript)
– Usa este decodificador de ejemplo:

function decodeUplink(input) {
  const bytes = input.bytes;
  if (bytes.length < 6) {
    return { data: { error: "payload too short" } };
  }
  const dist_mm = (bytes[0] << 8) | bytes[1];
  const vbus_mv = (bytes[2] << 8) | bytes[3];
  let cur_mA_x10 = (bytes[4] << 8) | bytes[5];
  if (cur_mA_x10 & 0x8000) cur_mA_x10 = cur_mA_x10 - 0x10000; // int16
  const cur_mA = cur_mA_x10 / 10.0;

  // Ejemplo de nivel estimado si conoces EMPTY/FULL (sincronizar con firmware si cambian)
  const EMPTY = 1500;
  const FULL = 100;
  const span = EMPTY - FULL;
  let level_pct = null;
  if (span > 0) {
    level_pct = Math.max(0, Math.min(100, Math.round(((EMPTY - dist_mm) * 100.0) / span)));
  }

  return {
    data: {
      distance_mm: dist_mm,
      level_percent: level_pct,
      vbus_mv: vbus_mv,
      current_mA: cur_mA
    }
  };
}
  • Guarda y prueba con un uplink recibido.

5) Verificación física:
– Mide con una regla la distancia del sensor al agua y compárala con “Distancia: … mm” en el monitor serie.
– Cambia el nivel del depósito (p. ej., añade agua) y confirma que:
– La distancia disminuye y el nivel (%) aumenta coherentemente.
– En TTN, la gráfica (si la configuras en un dashboard externo) refleja la evolución.

6) Telemetría de consumo:
– Cubre temporalmente el sensor para ver si la lectura cambia (el JSN‑SR04T puede cambiar su consumo según medición).
– Verifica que Vbus ~ 5 V y la corriente esté en el rango típico (20–70 mA para JSN‑SR04T, varía por versión).

7) Validación de duty-cycle y DR:
– En EU868, el duty-cycle limita la cadencia efectiva. Con TX_INTERVAL_SECONDS=300 no deberías tener problemas.
– Confirma data rate (DR) en el log y ajusta si la cobertura es pobre.

Troubleshooting

1) Join OTAA falla continuamente
– Causas:
– AppEUI/AppKey mal introducidos (orden o mayúsculas/minúsculas).
– Región incorrecta (LORAWAN_REGION no coincide con TTS).
– Antena no conectada o mala calidad de enlace.
– Soluciones:
– Duplica/pega de nuevo AppEUI/AppKey verificando longitud (16/32 hex).
– Ajusta región en secrets.h y recompila.
– Asegura antena y prueba cerca de una gateway conocida.
– Baja el data rate (EU868: DEFAULT_DATARATE=3 → SF9) para mayor alcance.

2) Lecturas del JSN‑SR04T a cero o “sin eco”
– Causas:
– Alimentación insuficiente (step‑up no da corriente suficiente).
– Divisor resistivo incorrecto (ECHO sigue a 5 V y la MKR no lee).
– Objetivo demasiado cercano o espuma intensa.
– Soluciones:
– Verifica Vbus~5 V e Ishunt con INA219; si cae la tensión, sube la capacidad del step‑up.
– Comprueba resistencias (10 kΩ en serie desde ECHO, 20 kΩ de pin a GND).
– Aumenta N_PINGS, reubica el sensor, o añade un tubo tranquilizador.

3) INA219 siempre lee 0 mA o valores erráticos
– Causas:
– Cableado V+/V- invertido (sensor no alimentado a través del shunt).
– Calibración inadecuada para el shunt real.
– Soluciones:
– Asegúrate de que la alimentación del JSN‑SR04T pasa por V+ → INA219 → V-.
– Cambia a setCalibration_32V_1A si trabajas con corrientes < 1 A y shunt de 0.1 Ω.

4) Uplinks no aparecen en TTS (pero el firmware dice “enviado”)
– Causas:
– Port o formato bloqueado por el decodificador.
– Gateway saturada o fuera de servicio.
– Soluciones:
– Asegura que FPort=2 (o el que uses), y que el payload no excede el tamaño máximo para ese DR.
– Prueba con payload más pequeño; acércate a la gateway y revisa Live Data.

5) Bloqueos al entrar en sleep o pérdida de sesión tras dormir
– Causas:
– Standby reconfigura periféricos; el módem puede requerir re‑sync.
– Soluciones:
– Mantén la lógica de re‑join en loop() como en el ejemplo (modem.connected()).
– Evita deep sleep excesivo antes de confirmar join inicial.

6) “Error: INA219 no encontrado”
– Causas:
– SDA/SCL invertidos, soldaduras flojas, alimentación a 5 V en VCC INA219 (usa 3.3 V).
– Soluciones:
– Verifica continuidad de SDA/SCL, que 3V3 alimenta VCC del INA219, y GND común.

7) Potencia de transmisión fuera de normativa
– Causa:
– DEFAULT_TXPOWER_DBM demasiado alta para tu región.
– Solución:
– Ajusta a 14 dBm (EU868) o según norma local; usa ADR cuando sea posible.

8) Distancia y nivel inconsistentes
– Causas:
– Calibración (EMPTY/FULL) no corresponde a la instalación real.
– Condensación en el sensor.
– Soluciones:
– Recalibra EMPTY_DISTANCE_MM y FULL_DISTANCE_MM sobre el terreno.
– Agrega un pequeño capuchón o protección anti‑condensación sin obstruir el haz.

Mejoras/variantes

  • Downlinks para configuración remota:
  • Implementa recepción de downlinks (puerto 10) para ajustar TX_INTERVAL_SECONDS, DR o límites EMPTY/FULL sin reprogramar.
  • Confirmed uplinks bajo evento:
  • Enviar uplink confirmado solo cuando el nivel cambia más de X %, manteniendo no confirmados para el resto.
  • Codificación CayenneLPP o JSON CBOR:
  • Si integras más sensores, un esquema estándar puede simplificar la decodificación en plataformas IoT.
  • Ahorro energético avanzado:
  • Añade un MOSFET P‑channel de alta‑lado para cortar la alimentación del JSN‑SR04T entre mediciones.
  • Usa temporizadores RTC y revisa “LowPower.deepSleep()” con re‑join programado.
  • Autodiagnóstico:
  • Incluye en el payload un “status byte” con bits de error (eco ausente, Vbus bajo, join fallido anterior).
  • Geometría del tanque:
  • Calcula volumen real (litros) a partir de la distancia y la forma (cilindro, prisma, irregular con tabla de calibración).
  • Seguridad:
  • Rota AppKey y usa device‑specific keys por fabricación.
  • Redundancia de medición:
  • Filtrado Kalman/MAD, o doble lectura con diferentes ventanas para mitigar espuma/salpicaduras.

Checklist de verificación

  • [ ] Antena LoRa conectada a la MKR WAN 1310 antes de energizar.
  • [ ] Step‑up 5 V conectado a LiPo (BAT) y GND común con MKR.
  • [ ] JSN‑SR04T alimentado desde INA219 (V+ del INA219 a 5 V del step‑up; V- a VCC del sensor).
  • [ ] Divisor resistivo en ECHO correcto (10 kΩ en serie desde ECHO, 20 kΩ de pin a GND).
  • [ ] TRIG conectado a D6, ECHO (dividido) a D7; SDA/SCL del INA219 a SDA/SCL de la MKR.
  • [ ] platformio.ini con las versiones exactas indicadas (atmelsam@8.2.0, framework-arduino-samd@1.8.13, etc.).
  • [ ] secrets.h con LORAWAN_REGION, AppEUI y AppKey correctos.
  • [ ] Compilación exitosa: pio run sin errores.
  • [ ] Carga exitosa en el puerto correcto (upload).
  • [ ] Join OTAA OK y uplinks visibles en TTS.
  • [ ] Decodificador funcionando (distancia_mm, level_percent, vbus_mv, current_mA).
  • [ ] Validación física del nivel: distancia coherente con medición manual.
  • [ ] Intervalo de transmisión y consumo dentro de expectativas.

Apéndice: Notas adicionales sobre precisión del JSN‑SR04T

  • El JSN‑SR04T está optimizado para exteriores y ambientes húmedos. Su lóbulo de haz es relativamente estrecho, pero la reflexión en superficies turbulentas puede variar.
  • Para depósitos con mucha espuma, un tubo tranquilizador (tubo vertical perforado) bajo el sensor reduce ruido.
  • Ajusta MAX_DISTANCE_CM al rango real para minimizar ecos espurios lejanos.
  • Si detectas lecturas “0 cm” es posible saturación o ningún eco; el filtro de mediana ayuda, pero revisa también la alimentación.

Apéndice: Consideraciones regulatorias

  • EU868: potencia máx. 14 dBm en la mayoría de sub‑bandas y duty‑cycle 1% típico. Respeta duty‑cycle mediante el intervalo de transmisión.
  • US915: no hay duty‑cycle pero sí limitación por hop y dwell time. Ajusta DR y sub‑bandas según la red.

Con este caso práctico, dispones de una cadena completa y reproducible para “lora‑water‑level‑telemetry” con el modelo exacto “Arduino MKR WAN 1310 + JSN‑SR04T + INA219”, cubriendo materiales, conexión, código, toolchain y validación integral.

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 exacta de Python requerida?




Pregunta 2: ¿Qué sistema operativo no es soportado según el artículo?




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




Pregunta 4: ¿Qué librería de Arduino tiene la versión 1.3.1?




Pregunta 5: ¿Qué tipo de sensor se requiere para el proyecto?




Pregunta 6: ¿Qué batería es recomendada para el proyecto?




Pregunta 7: ¿Cuál es la frecuencia recomendada para la antena LoRa?




Pregunta 8: ¿Qué resistor se usa para el divisor de nivel de 5 V a 3.3 V?




Pregunta 9: ¿Qué herramienta se usa para gestionar las librerías de Arduino?




Pregunta 10: ¿Qué driver se requiere en Windows para el Arduino MKR WAN 1310?




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:


Practical case: Arduino MKR WAN 1310 LoRaWAN water level

Practical case: Arduino MKR WAN 1310 LoRaWAN water level — hero

Objective and use case

What you’ll build: This project involves creating a LoRa water level telemetry system using the Arduino MKR WAN 1310, JSN-SR04T ultrasonic sensor, and INA219 power monitor for real-time water level monitoring.

Why it matters / Use cases

  • Remote monitoring of water levels in agricultural fields to optimize irrigation schedules.
  • Real-time water level tracking in reservoirs to prevent overflow and manage water resources effectively.
  • Deployment in flood-prone areas for early warning systems to alert communities of rising water levels.
  • Integration with smart city infrastructure for enhanced urban water management.

Expected outcome

  • Real-time transmission of water level data with latencies under 5 seconds.
  • Power consumption metrics reported by INA219, aiming for less than 50mA during active sensing.
  • Accurate water level measurements with a resolution of 1 cm, transmitted via LoRaWAN.
  • Successful data reception at a distance of up to 10 km in rural environments.

Audience: Engineers and developers interested in IoT applications; Level: Intermediate

Architecture/flow: Arduino MKR WAN 1310 collects data from JSN-SR04T, processes it, and sends it via LoRaWAN, while INA219 monitors power usage.

Advanced Practical Case: LoRa Water Level Telemetry with Arduino MKR WAN 1310 + JSN-SR04T + INA219

This hands-on build turns an Arduino MKR WAN 1310 into a robust LoRaWAN water-level telemetry node. It uses a waterproof ultrasonic sensor (JSN-SR04T) to measure distance to the water surface, converts that to level/volume, and transmits metrics via LoRaWAN. An INA219 current/power monitor is added to report sensor/system power usage for field diagnostics. You will compile and flash the sketch using Arduino CLI, not the GUI, with a clean, reproducible command sequence.

The project is engineered for real-world deployment: it includes filtering, calibration, low-power sleep, and a compact binary payload with a working decoder snippet for The Things Stack (TTS/TTN).

Device model: Arduino MKR WAN 1310 + JSN-SR04T + INA219
Objective: lora-water-level-telemetry


Prerequisites

  • Operating system: Linux/macOS/Windows (64-bit)
  • Installed tools:
  • Arduino CLI 0.34.2 or newer
  • Bash/PowerShell to run commands
  • A serial terminal (Arduino CLI monitor or screen)
  • LoRaWAN network access (e.g., The Things Stack Community or Enterprise)
  • An application and an OTAA device with JoinEUI/AppEUI and AppKey
  • Correct frequency plan/region for your deployment (e.g., EU868, US915)
  • Basic familiarity with:
  • Arduino pin I/O and I2C
  • LoRaWAN concepts: OTAA join, ADR, data rate
  • Safety handling for 5V signals on 3.3V MCUs (level shifting)

Driver notes:
– Arduino MKR WAN 1310 uses native USB (CDC). On Windows, install the “Arduino SAMD Boards” driver when prompted or via the Arduino IDE package if needed. No CP210x/CH34x drivers are required.


Materials (Exact Models)

  • 1x Arduino MKR WAN 1310 (exact model)
  • 1x JSN-SR04T waterproof ultrasonic distance sensor (v2 or v3; TTL variant)
  • 1x INA219 DC current/power monitor (Adafruit or equivalent breakout; default I2C address 0x40)
  • 1x 3.7V LiPo battery (optional but recommended for field)
  • 1x 5V source from MKR WAN 1310 5V pin (provided by USB/VIN; used to power JSN-SR04T)
  • Resistors for level shifting JSN-SR04T Echo line (exact values):
  • R1 = 10 kΩ (series from sensor Echo to MCU input node)
  • R2 = 20 kΩ (from MCU input node to GND)
  • Dupont wires and a small breadboard or terminal blocks
  • Outdoor-rated enclosure with cable glands (optional but recommended)
  • Antenna for the MKR WAN 1310’s LoRa radio (as supplied with the board)

Library versions (tested):
– arduino:samd core 1.8.13
– MKRWAN library 1.1.0
– ArduinoLowPower 1.2.2
– Adafruit INA219 1.2.1

Note: If your environment provides newer versions, they should still work; pinning these ensures reproducibility.


Setup/Connection

The MKR WAN 1310 is a 3.3V logic microcontroller. The JSN-SR04T requires 5V power and outputs a 5V Echo pulse. Do not connect the 5V Echo directly to the MKR pin. Use the resistor divider below.

Pin Planning

  • JSN-SR04T
  • VCC: 5V (from MKR 5V pin)
  • GND: GND
  • TRIG: MKR pin D6 (3.3V output is fine)
  • ECHO: MKR pin D7 via R1/R2 divider (5V to ~3.3V)
  • INA219 (I2C)
  • VIN+: 5V source side (from MKR 5V pin)
  • VIN-: Load side to JSN-SR04T VCC
  • SDA: MKR SDA (pin labeled SDA)
  • SCL: MKR SCL (pin labeled SCL)
  • GND: MKR GND
  • VCC: 3.3V (preferred) or 5V; Adafruit INA219 supports 3.3V logic, so tie to 3.3V for clean logic levels

Electrical Connections Table

Component Pin/Label Connects To Notes
MKR WAN 1310 5V INA219 VIN+ 5V supply path to sensor via INA219 shunt
INA219 VIN- JSN-SR04T VCC Measures current/power of the ultrasonic sensor
MKR WAN 1310 GND INA219 GND, JSN-SR04T GND Common ground
MKR WAN 1310 3.3V INA219 VCC Power the INA219 logic at 3.3V
MKR WAN 1310 SDA INA219 SDA I2C data
MKR WAN 1310 SCL INA219 SCL I2C clock
MKR WAN 1310 D6 JSN-SR04T TRIG Trigger output to sensor
MKR WAN 1310 D7 JSN-SR04T ECHO via divider R1=10 kΩ from ECHO to D7 node; R2=20 kΩ from D7 node to GND; ensures ~3.3V at the MCU

Important:
– Place the INA219 shunt in series only with the JSN-SR04T VCC line; this allows measuring the sensor’s consumption without interfering with the MKR’s own power path.
– Ensure the MKR’s antenna is attached and outside any metal enclosure before joining the network.
– Keep the JSN-SR04T face unobstructed and at least 20 cm from walls for accurate readings.


Full Code

We will use a small project layout:

  • project folder: lora-water-level-telemetry/
  • lora-water-level-telemetry.ino (main)
  • secrets.h (your LoRaWAN keys; not committed)

The payload format:
– Byte 0..1: distance_mm (uint16, 0–65535)
– Byte 2..3: bus_mV (uint16, INA219 bus voltage in millivolts)
– Byte 4..5: current_mA_x10 (uint16, INA219 current in 0.1 mA units)
– Byte 6..7: volume_liters (uint16, computed volume; 1 L resolution)
– Byte 8: level_percent (uint8, 0–100)

Adjust parameters in the code for your installation: tank height, sensor offset, calibration, region, and join keys.

secrets.h (template; create and fill with your keys)

// secrets.h - do not commit
#pragma once

// Set your LoRaWAN region in the main sketch (e.g., "EU868", "US915")

// OTAA credentials from The Things Stack (TTS/TTN)
static const char* APP_EUI = "70B3D57EDXXXXXXX"; // also called JoinEUI (hex string, no spaces)
static const char* APP_KEY = "5C8A...F2B";       // 16-byte hex string, 32 hex chars

// Optional: if your network requires DevEUI to be set manually, define it:
static const char* DEV_EUI = ""; // leave empty to use onboard value if supported

lora-water-level-telemetry.ino

/*
  LoRa Water Level Telemetry
  Hardware: Arduino MKR WAN 1310 + JSN-SR04T + INA219
  Features:
    - JSN-SR04T distance measurement with median filtering
    - Tank geometry conversion to level (%) and volume (L)
    - INA219 current/power of the JSN sensor line
    - LoRaWAN OTAA join and uplink payload
    - Low power sleep between measurements
*/

#include <MKRWAN.h>
#include <ArduinoLowPower.h>
#include <Wire.h>
#include <Adafruit_INA219.h>
#include "secrets.h"

// ---------------------- Region and LoRaWAN ----------------------
String REGION = "EU868"; // set to "EU868", "US915", "AS923", etc.
LoRaModem modem;

// LoRa settings
const bool USE_ADR = true;
const bool CONFIRMED = false;  // use unconfirmed to save airtime unless you need ACK
int DR = 3;                    // data rate; depends on region, tune as needed

// ---------------------- Sensor Pins -----------------------------
const int PIN_TRIG = 6;  // D6
const int PIN_ECHO = 7;  // D7 via 10k/20k divider

// ---------------------- Timing ------------------------------
const uint32_t MEAS_INTERVAL_SEC = 300; // 5 minutes
const uint8_t SAMPLES = 7;              // median of 7 for robustness

// ---------------------- Tank Calibration --------------------
const float TANK_HEIGHT_MM = 2000.0f;    // distance from sensor face to tank bottom in mm
const float SENSOR_OFFSET_MM = 45.0f;    // dead zone/holder offset; subtract from measured distance
const float TANK_DIAMETER_MM = 1000.0f;  // cylindrical tank example
// Use a simple cylinder. For complex geometries, replace volume calculation.
float litersFromHeight(float waterHeightMm) {
  // Cylinder volume V = pi * r^2 * h; convert mm^3 to L
  const float r_mm = TANK_DIAMETER_MM / 2.0f;
  double mm3 = 3.14159265358979323846 * r_mm * r_mm * (double)waterHeightMm;
  return (float)(mm3 / 1e6); // 1e6 mm^3 per liter
}

// ---------------------- INA219 -------------------------------
Adafruit_INA219 ina219; // default address 0x40

// ---------------------- Utilities ----------------------------
static uint16_t clampU16(int32_t v) { if (v < 0) return 0; if (v > 65535) return 65535; return (uint16_t)v; }

uint32_t pulseInTimeout(uint8_t pin, uint8_t state, uint32_t timeout_us) {
  // Safe pulseIn with timeout
  return pulseIn(pin, state, timeout_us);
}

float measureDistanceMm() {
  // Trigger the JSN-SR04T and measure echo duration
  digitalWrite(PIN_TRIG, LOW);
  delayMicroseconds(3);
  digitalWrite(PIN_TRIG, HIGH);
  delayMicroseconds(12);
  digitalWrite(PIN_TRIG, LOW);

  // Echo timeout at e.g., 30 ms = ~5 m of range (speed of sound ~343 m/s)
  uint32_t duration = pulseInTimeout(PIN_ECHO, HIGH, 30000UL);
  if (duration == 0) return NAN;

  // Distance in cm: duration_us / 58.2; convert to mm
  float distance_mm = (float)duration / 58.2f * 10.0f;
  return distance_mm;
}

float medianOfSamples(uint8_t n) {
  float buf[15]; // up to 15 samples
  if (n > 15) n = 15;
  uint8_t count = 0;
  for (uint8_t i = 0; i < n; i++) {
    float d = measureDistanceMm();
    if (!isnan(d) && d > 30 && d < 6000) { // plausible gate
      buf[count++] = d;
    }
    delay(60);
  }
  if (count == 0) return NAN;
  // insertion sort
  for (uint8_t i = 1; i < count; i++) {
    float key = buf[i];
    int j = i - 1;
    while (j >= 0 && buf[j] > key) { buf[j+1] = buf[j]; j--; }
    buf[j+1] = key;
  }
  return buf[count / 2];
}

bool readINA219(float &busVolts, float &currentmA) {
  busVolts = ina219.getBusVoltage_V(); // bus voltage (V)
  currentmA = ina219.getCurrent_mA();  // current (mA)
  return true;
}

void sleepSeconds(uint32_t s) {
  // Sleep in 8s chunks due to library constraints
  while (s >= 8) { LowPower.sleep(8000); s -= 8; }
  if (s > 0) { LowPower.sleep(s * 1000); }
}

void setupSerial() {
  Serial.begin(115200);
  while (!Serial && millis() < 4000) { ; }
}

// ---------------------- Setup -------------------------------
void setup() {
  setupSerial();
  pinMode(PIN_TRIG, OUTPUT);
  pinMode(PIN_ECHO, INPUT);
  digitalWrite(PIN_TRIG, LOW);

  Wire.begin();
  if (!ina219.begin()) {
    Serial.println("INA219 not found. Check wiring.");
  } else {
    // Configure INA219 calibration; default is ~0.1 ohm shunt on breakout
    ina219.setCalibration_32V_2A();
  }

  // LoRa modem init
  if (!modem.begin(REGION)) {
    Serial.println("Failed to start LoRa modem. Check region/antenna.");
    while (1) { delay(1000); }
  }
  Serial.print("Modem version: "); Serial.println(modem.version());
  Serial.print("Device EUI: "); Serial.println(modem.deviceEUI());

  // ADR and DR
  modem.setADR(USE_ADR);
  if (!USE_ADR) modem.dataRate(DR);

  // OTAA join
  int connected = 0;
  if (strlen(DEV_EUI) > 0) {
    modem.setDevEUI(DEV_EUI);
  }
  Serial.println("Joining LoRaWAN...");
  connected = modem.joinOTAA(APP_EUI, APP_KEY);
  if (!connected) {
    Serial.println("Join failed. Retrying with a slow backoff...");
    for (int i = 0; i < 5 && !connected; i++) {
      delay(5000 * (i + 1));
      connected = modem.joinOTAA(APP_EUI, APP_KEY);
    }
  }
  if (!connected) {
    Serial.println("Join failed permanently. Check keys/region.");
    // You might still want to continue to allow offline validation.
  } else {
    Serial.println("Joined LoRaWAN.");
  }

  // Set low power mode for modem if supported
  modem.minPollInterval(60); // reduce network overhead
}

// ---------------------- Loop -------------------------------
void loop() {
  // Measure distance with median filtering
  float raw_mm = medianOfSamples(SAMPLES);
  float busV = NAN, curmA = NAN;
  readINA219(busV, curmA);

  float corrected_mm = raw_mm - SENSOR_OFFSET_MM;
  if (isnan(raw_mm) || corrected_mm < 0) corrected_mm = 0;

  // Convert to water height above bottom
  float height_mm = TANK_HEIGHT_MM - corrected_mm;
  if (height_mm < 0) height_mm = 0;
  if (height_mm > TANK_HEIGHT_MM) height_mm = TANK_HEIGHT_MM;

  float liters = litersFromHeight(height_mm);
  uint8_t level_pct = (uint8_t)((height_mm / TANK_HEIGHT_MM) * 100.0f + 0.5f);

  // Build payload
  uint8_t payload[9];
  uint16_t dist_u16 = clampU16((int32_t)(corrected_mm + 0.5f));
  uint16_t mv_u16 = clampU16((int32_t)(busV * 1000.0f + 0.5f));
  uint16_t cur_x10 = clampU16((int32_t)(curmA * 10.0f + 0.5f));
  uint16_t liters_u16 = clampU16((int32_t)(liters + 0.5f));

  payload[0] = (dist_u16 >> 8) & 0xFF;
  payload[1] = (dist_u16) & 0xFF;
  payload[2] = (mv_u16 >> 8) & 0xFF;
  payload[3] = (mv_u16) & 0xFF;
  payload[4] = (cur_x10 >> 8) & 0xFF;
  payload[5] = (cur_x10) & 0xFF;
  payload[6] = (liters_u16 >> 8) & 0xFF;
  payload[7] = (liters_u16) & 0xFF;
  payload[8] = level_pct;

  // Diagnostics to serial
  Serial.print("Raw(mm)="); Serial.print(raw_mm, 1);
  Serial.print(" Corrected(mm)="); Serial.print(corrected_mm, 1);
  Serial.print(" Height(mm)="); Serial.print(height_mm, 1);
  Serial.print(" Level(%)="); Serial.print(level_pct);
  Serial.print(" Volume(L)="); Serial.print(liters, 1);
  Serial.print(" BusV(V)="); Serial.print(busV, 3);
  Serial.print(" I(mA)="); Serial.println(curmA, 2);

  // Transmit
  int err = modem.beginPacket();
  if (err <= 0) {
    Serial.println("beginPacket failed");
  } else {
    modem.write(payload, sizeof(payload));
    err = modem.endPacket(CONFIRMED);
    if (err > 0) {
      Serial.println("Uplink OK");
    } else {
      Serial.print("Uplink failed ("); Serial.print(err); Serial.println(")");
    }
  }

  // Sleep to save power
  sleepSeconds(MEAS_INTERVAL_SEC);
}

Optional TTS/TTN payload formatter (JavaScript) for your application:

// The Things Stack (v3) Uplink Decoder
function decodeUplink(input) {
  const bytes = input.bytes;
  if (bytes.length < 9) {
    return { errors: ["payload too short"] };
  }
  const dist = (bytes[0] << 8) | bytes[1];
  const mv = (bytes[2] << 8) | bytes[3];
  const cur_x10 = (bytes[4] << 8) | bytes[5];
  const liters = (bytes[6] << 8) | bytes[7];
  const level = bytes[8];

  return {
    data: {
      distance_mm: dist,
      bus_mV: mv,
      current_mA: cur_x10 / 10.0,
      volume_l: liters,
      level_percent: level
    }
  };
}

Build/Flash/Run Commands

Use Arduino CLI with the correct FQBN for MKR WAN 1310: arduino:samd:mkrwan1310

On first use, install the core and libraries.

arduino-cli version

# 2) Configure Arduino CLI (create config if needed)
arduino-cli config init

# 3) Update index and install SAMD core for MKR WAN 1310
arduino-cli core update-index
arduino-cli core install arduino:samd@1.8.13

# 4) Install required libraries (pin versions for reproducibility)
arduino-cli lib install "MKRWAN@1.1.0"
arduino-cli lib install "ArduinoLowPower@1.2.2"
arduino-cli lib install "Adafruit INA219@1.2.1"

# 5) Create project folder structure
mkdir -p ~/projects/lora-water-level-telemetry
cd ~/projects/lora-water-level-telemetry

# 6) Place the two files:
#    - lora-water-level-telemetry.ino
#    - secrets.h
#    in the current directory.

# 7) Detect board and port
arduino-cli board list

# Example output:
# Port         Type              FQBN                       Core
# /dev/ttyACM0 Serial Port (USB) arduino:samd:mkrwan1310    arduino:samd
# On Windows this might be COM7, e.g., COM7

# 8) Compile
arduino-cli compile --fqbn arduino:samd:mkrwan1310 .

# 9) Upload (replace PORT accordingly)
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:samd:mkrwan1310 .

# 10) Open serial monitor at 115200 baud to watch logs
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

If you get permission errors on Linux for /dev/ttyACM0, add your user to the dialout group and re-login:
– sudo usermod -a -G dialout $USER


Step-by-step Validation

Follow these steps to verify each subsystem and the end-to-end telemetry.

1) Local serial diagnostics

  • Power the board via USB with the antenna attached.
  • Open the serial monitor:
  • arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200
  • On boot, you should see:
  • Modem version and Device EUI printed
  • “Joining LoRaWAN…” then “Joined LoRaWAN.” (if network reachable and keys are correct)
  • A measurement line every MEAS_INTERVAL_SEC seconds:
    • Raw(mm), Corrected(mm), Height(mm), Level(%), Volume(L), BusV(V), I(mA)
  • Move a flat object above the JSN-SR04T at known distances (e.g., 30 cm, 50 cm) and confirm that:
  • Raw and Corrected distances match expectations within ±1–2 cm
  • Level% changes accordingly if your TANK_HEIGHT_MM is configured

Tip: For bench testing without a tank, set TANK_HEIGHT_MM to a smaller value (e.g., 600 mm) to make level% changes more obvious.

2) JSN-SR04T sanity checks

  • If Raw(mm) reads NAN or 0 frequently:
  • Ensure the Echo line is level-shifted with the 10k/20k divider
  • Verify 5V is stable at the sensor VCC pin
  • Increase the pulseIn timeout slightly in measureDistanceMm() if your tank is deep
  • Confirm the sensor face is clean and not tilted with respect to the water surface.

3) INA219 readings

  • Check BusV ~ 5.0 V (may be 4.7–5.2 V depending on USB source).
  • With the JSN-SR04T idle, current may be a few mA; during burst, it spikes higher.
  • If current is always 0:
  • Confirm INA219 address (0x40 default) and wiring (VIN+, VIN-, and VCC=3.3V)
  • Confirm ina219.begin() succeeded (noted on serial)

4) LoRaWAN join and uplink

  • In The Things Stack console:
  • Ensure the device is registered under your application with OTAA.
  • Frequency plan must match REGION in the sketch.
  • After reset, the device should issue a join request, then an uplink.
  • Install the provided uplink decoder in the application’s “Payload formatters” (Uplink) and click Save.
  • In the “Live data” view:
  • After the first measurement, confirm an uplink packet arrives every 5 minutes.
  • Verify decoded fields:
    • distance_mm increases when the water surface moves away from the sensor
    • level_percent reflects tank level
    • bus_mV ≈ ~5000 mV
    • current_mA shows spikes consistent with ultrasonic bursts

5) Tank calibration

  • Measure the actual distance from sensor face to the tank bottom and set TANK_HEIGHT_MM.
  • If the sensor is recessed in a mount or has a dead zone, measure and set SENSOR_OFFSET_MM.
  • Fill the tank to a known fraction (e.g., 50%) and compare level_percent. If off by a constant scale:
  • Check TANK_DIAMETER_MM (if your tank is cylindrical).
  • For rectangular tanks, replace litersFromHeight() with widthlengthheight conversion.
  • Record multiple fill points to verify linearity.

6) End-to-end sanity

  • Confirm consistent uplinks for at least 30–60 minutes.
  • Confirm ADR status on the network (if enabled) and that data rate remains appropriate for link quality.
  • Ensure no MAC command errors or frame counter mismatches occur in the console.

Troubleshooting

  • Join fails or never uplinks:
  • Check antenna is connected and placed away from metal.
  • Verify REGION matches your frequency plan (e.g., EU868 vs US915). Change REGION string in setup.
  • Ensure APP_EUI (JoinEUI) and APP_KEY are correct hex strings with no spaces.
  • Some networks require setting DevEUI explicitly; fill DEV_EUI if needed (check TTS device page).
  • Verify gateway coverage and downlink availability for OTAA. If in a shielded room, move closer to a window.

  • Uplinks arrive but no decoded fields:

  • Ensure the payload decoder is saved and has no syntax errors.
  • Confirm payload size is 9 bytes.

  • Distance readings inconsistent or stuck:

  • JSN-SR04T needs a clean acoustic path; condensation or turbulent surface can cause jitter.
  • Increase SAMPLES or add a larger median window.
  • Reduce environmental acoustic noise and avoid mounting close to tank walls.
  • Ensure trigger pulse width is adequate (10–12 µs is typical) and that TRIG pin is defined as OUTPUT.

  • Echo line damages or erratic input:

  • Never connect 5V Echo directly to MKR pin. Use the exact 10k (series, top) and 20k (to GND, bottom) divider.
  • Check with a multimeter: in HIGH, the divider node should read close to 3.3V.

  • INA219 zero or unrealistic values:

  • Confirm the shunt is actually in series with the JSN-SR04T VCC line (VIN+ to 5V source, VIN- to sensor VCC).
  • Power INA219 at 3.3V so logic levels are coherent with MKR’s I2C.
  • If using a different breakout, note its shunt value and adjust calibration (e.g., setCalibration_32V_1A).

  • Power budget and resets:

  • When powered by USB only, 5V may sag with poor cables. Try a better USB cable or powered hub.
  • If running on LiPo, ensure the battery is charged and check charge status LEDs on MKR WAN 1310.

Improvements

  • Downlink configuration:
  • Add a small downlink parser to allow changing MEAS_INTERVAL_SEC or DR on the fly (e.g., via port 2).
  • Implement a command to trigger an immediate measurement.

  • Advanced filtering and reliability:

  • Implement outlier rejection using interquartile range and adaptive timeouts.
  • Add temperature compensation for speed of sound. If you have a temperature sensor, adjust c = 331 + 0.6*T(m/s).

  • Power optimization:

  • Power-gate the JSN-SR04T via a P-MOSFET controlled by an MKR pin to remove idle draw between measurements.
  • Increase sleep duration and switch to confirmed uplinks only for alerts.

  • Payload optimization:

  • Use a packed bitfield to shrink the payload further, or adopt CayenneLPP if you prefer tooling compatibility.
  • Add battery voltage of the MKR board if you bring it into measurement via an analog divider (3.3V-limited).

  • Geometry generalization:

  • Replace the cylinder model with piecewise linear or lookup-table calibration for irregular tanks.

  • Robustness:

  • Add a watchdog timer reset if join/uplink fails N times.
  • CRC32 of the data pre-transmission for integrity (not needed for LoRaWAN but useful for internal checks).

  • Cloud integration:

  • Push decoded data to a time-series database (InfluxDB) and visualize with Grafana via TTS MQTT.

Final Checklist

  • Hardware
  • Antenna firmly connected to MKR WAN 1310.
  • JSN-SR04T VCC=5V, GND common, TRIG=D6, ECHO to D7 through 10k/20k divider.
  • INA219 wired: VIN+ to 5V source, VIN- to sensor VCC; SDA/SCL to MKR; VCC=3.3V; GND common.
  • Enclosure protects from moisture; sensor face unobstructed.

  • Software

  • Arduino CLI installed and working.
  • Core arduino:samd@1.8.13 installed.
  • Libraries installed: MKRWAN 1.1.0, ArduinoLowPower 1.2.2, Adafruit INA219 1.2.1.
  • secrets.h contains correct APP_EUI/APP_KEY (and DEV_EUI if required).
  • REGION matches your frequency plan (e.g., EU868 or US915).
  • FQBN arduino:samd:mkrwan1310 used for compile/upload.

  • Validation

  • Serial logs show successful join (or reason for failure) and periodic measurements.
  • JSN-SR04T distance values vary appropriately with target range.
  • INA219 reports plausible bus voltage (~5000 mV) and current.
  • TTS/TTN receives uplinks with 9-byte payload and decodes fields correctly.
  • Level% and liters track real tank conditions after calibration.

  • Deployment

  • Sleep interval set to meet duty cycle and battery goals.
  • ADR enabled if stationary and within a stable network coverage area.
  • Alarm thresholds considered and possibly implemented via downlink or local logic.

With these steps complete, you have a dependable LoRaWAN water level telemetry node built on Arduino MKR WAN 1310 with ultrasonic sensing and power diagnostics, ready for field deployment.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What type of sensor is used in the project?




Question 2: Which Arduino model is utilized for the telemetry node?




Question 3: What is the purpose of the INA219 in the project?




Question 4: Which command-line interface is used to compile and flash the sketch?




Question 5: What type of network access is required for the project?




Question 6: What is the main objective of the project?




Question 7: What operating systems are supported for this project?




Question 8: What is the significance of OTAA in LoRaWAN?




Question 9: Which component is NOT required for this project?




Question 10: What is the function of the filtering and calibration mentioned in the project?




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

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me: