Caso práctico: Hexápodo Arduino Mega 2560 + PCA9685 + XBee

Caso práctico: Hexápodo Arduino Mega 2560 + PCA9685 + XBee — hero

Objetivo y caso de uso

Qué construirás: Un robot hexápodo controlado por Zigbee utilizando Arduino Mega 2560, PCA9685 y XBee S2C.

Para qué sirve

  • Control remoto de un hexápodo para exploración en terrenos difíciles.
  • Implementación de evitación de obstáculos utilizando el sensor VL53L0X.
  • Demostraciones de robótica educativa en ferias tecnológicas.
  • Proyectos de investigación en movilidad robótica y comunicación inalámbrica.

Resultado esperado

  • Latencia de control remoto inferior a 100 ms.
  • Capacidad de respuesta a comandos en tiempo real con un mínimo de 10 FPS.
  • Precisión de detección de obstáculos de hasta 30 cm con el sensor ToF.
  • Consumo de energía del sistema menor a 500 mA durante la operación.

Público objetivo: Estudiantes avanzados y entusiastas de la robótica; Nivel: Avanzado

Arquitectura/flujo: Arduino Mega 2560 controla el PCA9685 para manejar los servos, mientras que el XBee S2C se encarga de la comunicación Zigbee y el VL53L0X proporciona datos de distancia para la evitación de obstáculos.

Nivel: Avanzado

Prerrequisitos

  • Sistema operativo recomendado:
  • Ubuntu 22.04.4 LTS (64-bit) o equivalente en Linux con soporte para USB CDC.
  • Alternativa: Windows 10/11 o macOS 12+, con adaptaciones de puerto serie.

  • Toolchain exacta (probada):

  • Arduino CLI 0.35.3
  • Core Arduino AVR Boards arduino:avr@1.8.6
  • avr-gcc 7.3.0-atmel3.6.1
  • avrdude 6.3
  • Bibliotecas Arduino (Arduino Library Manager):
  • Adafruit PWM Servo Driver Library@2.4.1
  • Adafruit BusIO@1.14.5
  • Adafruit Unified Sensor@1.1.14
  • Adafruit VL53L0X@1.3.1
  • Python 3.10.12 (opcional, para script de comandos por ZigBee)
  • minicom 2.7.1 (o picocom) para terminal serie en Linux, o PuTTY en Windows

  • Hardware principal del caso:

  • Arduino Mega 2560
  • PCA9685 (driver 16 canales servo por I2C)
  • XBee S2C (Zigbee) en modo transparente (AP=0)
  • VL53L0X (sensor ToF por I2C)
  • Objetivo del proyecto: servo-hexapod-zigbee (hexápodo de 18 servos con control remoto Zigbee y evitación de obstáculos con ToF).

  • Notas de compatibilidad:

  • Arduino Mega 2560 usa 5 V lógicos; el XBee S2C opera a 3.3 V. Es obligatorio un shield con conversión de nivel o un conversor bidireccional (TX/RX).
  • PCA9685 y VL53L0X comparten el bus I2C (SDA/SCL); asegúrate de que sus direcciones no colisionen (por defecto PCA9685 0x40, VL53L0X 0x29).

Materiales

  • Electrónica y módulos:
  • 1x Arduino Mega 2560 (original o compatible, con USB).
  • 1x PCA9685 16-channel 12-bit PWM Servo Driver (dirección por defecto 0x40).
  • 1x XBee S2C Zigbee (serie S2C, 2.4 GHz).
  • 1x Shield/adaptador XBee para Arduino Mega (con conversión de nivel a 3.3 V) o conversor de nivel bidireccional para UART.
  • 1x VL53L0X (módulo ToF, 940 nm).
  • 18x servos estándar para robótica (ej.: SG90/MG90S para prototipo o servos metálicos 12–20 kg·cm para carga real).
  • 1x Fuente DC 6V–7.4 V con capacidad ≥ 5–10 A (según servos).
  • 1x Portapilas LiPo 2S/3S o fuente conmutada 6 V regulada de alta corriente.
  • Cableado Dupont, regletas, tornillería y bridas.
  • 1x Chasis de hexápodo (6 patas, 3 DOF por pata: coxa, fémur, tibia).
  • 1x Fusible/limitador o protector de corriente para la línea de servos (recomendado).
  • 1x Interruptor general en el bus de servos (recomendado).

  • Opcionales útiles:

  • 1x Módulo UBEC 5 V/6 V de 5–10 A para servos.
  • Pasta térmica/adhesivo para disipación del PCA9685 (no siempre necesario, pero útil).

  • Software:

  • Arduino CLI 0.35.3 (instalar por script oficial).
  • Librerías especificadas (vías Arduino CLI).
  • minicom/picocom o Python para pruebas Zigbee.

Preparación y conexión

Consideraciones eléctricas

  • Los servos NO deben alimentarse desde el pin 5V del Arduino Mega. Usa una fuente externa para la línea V+ del PCA9685 (pines “V+” de potencia).
  • Une siempre GND del Arduino, GND del PCA9685 y GND de la fuente de servos. Masa común es imprescindible para estabilidad y PWM correcto.
  • El XBee S2C debe recibir 3.3 V y señales TX/RX a 3.3 V. Emplea un shield XBee o conversores de nivel. No conectes el XBee directamente a 5 V.

Mapeo de pines/puertos

  • I2C en Arduino Mega 2560:
  • SDA: pin 20
  • SCL: pin 21

  • UART para XBee:

  • Serial1: RX1 pin 19, TX1 pin 18 (recomendado para dejar Serial0/USB para logs).

  • Alimentación:

  • PCA9685: VCC a 5 V del Mega (lógica), GND común; V+ (poder) a 6 V–7.4 V externa para servos.
  • VL53L0X: VCC a 5 V si el módulo tiene regulador/level shifting (común en breakout comerciales), o 3.3 V si es bare.

Tabla de conexiones

Módulo Pin/Señal módulo Conecta a (Mega/otro) Notas
PCA9685 VCC 5V Mega Lógica
PCA9685 GND GND común Común a todo
PCA9685 SDA SDA (20) Mega I2C
PCA9685 SCL SCL (21) Mega I2C
PCA9685 V+ (barral) +6–7.4 V externa Potencia servos
PCA9685 GND (barral) GND fuente servos Común
VL53L0X VCC 5V (si trae regulador) Ver ficha módulo
VL53L0X GND GND común
VL53L0X SDA SDA (20) Mega I2C
VL53L0X SCL SCL (21) Mega I2C
XBee S2C VCC 3.3 V del shield No 5 V
XBee S2C GND GND común
XBee S2C DOUT (TX del XBee) RX1 (19) Mega (via shield/level) UART
XBee S2C DIN (RX del XBee) TX1 (18) Mega (via shield/level) UART
Servos (18) Señal Canales PCA9685 0–17 Ver mapeo abajo
Servos +V V+ PCA9685 6–7.4 V
Servos GND GND PCA9685

Mapeo de servos a canales (hexápodo 6 patas x 3 DOF)

  • Convención de patas: L0, L1, L2 (lado izquierdo front–middle–rear), R0, R1, R2 (lado derecho front–middle–rear).
  • DOF por pata: coxa (rotación horizontal), fémur (levanta/baja), tibia (extiende).
Pata Coxa (canal) Fémur (canal) Tibia (canal)
L0 0 1 2
L1 3 4 5
L2 6 7 8
R0 9 10 11
R1 12 13 14
R2 15 16 17
  • Ajusta los canales si tu cableado físico difiere, pero mantén una estructura coherente para simplificar el control y las gait tables.

Configuración básica del XBee S2C (modo transparente)

  • Conecta el XBee remoto a tu PC por un adaptador USB-XBee (3.3 V).
  • Entra en modo de comandos “AT” a 115200 bps (por defecto puede venir a 9600 bps; ajusta según sea necesario):
  • Abre minicom/picocom: a 9600 o 115200 según módulo.
  • Teclea “+++” y espera “OK”.
  • Configura (ejemplo: PAN en 0x1234, canal C, router/coordinator según topología). Para control remoto simple, haz que el robot sea “Router” (CE=0) y el XBee de la PC “Coordinator” (CE=1):
  • ATRE
  • ATAP0 (modo transparente)
  • ATBD7 (115200 bps)
  • ATCE1 (coordinator en el remoto de PC) o ATCE0 (router en el robot)
  • ATID1234 (PAN ID)
  • ATCH0C (canal 0x0C, opcional)
  • ATWR (guardar)
  • ATCN (salir)
  • En el lado del robot (XBee en el shield del Mega), aplica:
  • ATAP0
  • ATBD7
  • ATCE0
  • ATID1234
  • ATWR
  • Nota: si prefieres API mode (AP=2), modifica el código para usar frames; en este caso práctico usamos transparente para mantener el enfoque en gait + sensórica.

Código completo (Arduino C++)

El siguiente sketch controla:
– PCA9685 a 50 Hz para 18 servos.
– Gait tipo “trípode” con temporización no bloqueante.
– Parser de comandos por Zigbee (Serial1) con texto: FWD, BACK, LEFT, RIGHT, STOP, STEP, SPEED n, CALIB leg dof offset.
– VL53L0X para evitación: detiene y notifica si hay obstáculo < 200 mm.

Guarda el archivo como servo_hexapod_zigbee/servo_hexapod_zigbee.ino.

#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
#include <Adafruit_VL53L0X.h>
#include <Adafruit_Sensor.h>

// ---------- Configuración PCA9685 ----------
static const uint8_t PCA_ADDR = 0x40;
static const float SERVO_FREQ = 50.0f;     // Hz

Adafruit_PWMServoDriver pca = Adafruit_PWMServoDriver(PCA_ADDR);

// Rango típico de servo en microsegundos:
static const int SERVO_MIN_US = 500;
static const int SERVO_MAX_US = 2500;

// Canales por pata (coxa, femur, tibia)
struct LegMap { uint8_t coxa, femur, tibia; };
LegMap legs[6] = {
  {0, 1, 2},     // L0
  {3, 4, 5},     // L1
  {6, 7, 8},     // L2
  {9, 10, 11},   // R0
  {12, 13, 14},  // R1
  {15, 16, 17}   // R2
};

// Offsets por servo para centrado (us). Ajusta tras calibración.
int16_t servoOffsetUs[18] = {
  0,0,0,  0,0,0,  0,0,0,  0,0,0,  0,0,0,  0,0,0
};

// Inversión por servo (1 normal, -1 invertido)
int8_t servoDir[18] = {
  1, -1, -1,   // L0: coxa normal, femur invertido, tibia invertida
  1, -1, -1,   // L1
  1, -1, -1,   // L2
  -1, -1, -1,  // R0: coxa invertida (simetría), femur invertido, tibia invertida
  -1, -1, -1,  // R1
  -1, -1, -1   // R2
};

// ---------- Cinemática sencilla (3 DOF por pata) ----------
struct Vec3 { float x, y, z; };

// Longitudes (mm) de cada link. Ajusta a tu chasis.
static const float L_COXA = 28.0f;
static const float L_FEMUR = 55.0f;
static const float L_TIBIA = 85.0f;

// Límites de ángulo por DOF (grados)
static const float COXA_MIN = -45.0f, COXA_MAX = 45.0f;
static const float FEMUR_MIN = -20.0f, FEMUR_MAX = 75.0f;
static const float TIBIA_MIN = -70.0f, TIBIA_MAX = 20.0f;

// Conversión de ángulo (grados) a microsegundos
int angleToUs(float angleDeg) {
  // Mapear [0..180] a [SERVO_MIN_US..SERVO_MAX_US]
  // Primero acotamos a 0..180 desde una referencia 90 en neutro
  float a = angleDeg;
  if (a < 0) a = 0;
  if (a > 180) a = 180;
  float us = SERVO_MIN_US + (a / 180.0f) * (SERVO_MAX_US - SERVO_MIN_US);
  return (int)us;
}

// Conversión us -> ticks PCA9685
uint16_t usToTicks(int us) {
  float ticks = (SERVO_FREQ * 4096.0f * (float)us) / 1000000.0f;
  if (ticks < 0) ticks = 0;
  if (ticks > 4095) ticks = 4095;
  return (uint16_t)ticks;
}

// Set de un canal con ángulo absoluto (0..180), más offset e inversión
void setServoAngle(uint8_t ch, float angleDeg) {
  // Inversión por hardware lógico
  float a = angleDeg;
  if (servoDir[ch] < 0) a = 180.0f - a;
  int baseUs = angleToUs(a);
  int us = baseUs + servoOffsetUs[ch];
  uint16_t t = usToTicks(us);
  pca.setPWM(ch, 0, t);
}

// IK de una pata: retorna ángulos en grados (coxa, femur, tibia)
bool ikSolve(const Vec3 &target, float &aCoxa, float &aFemur, float &aTibia, bool isRightSide) {
  // Ajuste horizontal: coxa rota en planta por atan2(y, x)
  float a = atan2f(target.y, target.x); // rad
  aCoxa = a * 180.0f / M_PI; // grados

  // Proyección al plano de fémur-tibia: dist desde el eje de coxa
  float r = sqrtf(target.x*target.x + target.y*target.y) - L_COXA;
  float z = target.z;

  // Distancia al pie en el plano (r,z)
  float d = sqrtf(r*r + z*z);

  // Ley del coseno para aFemur y aTibia
  float cosTibia = (L_FEMUR*L_FEMUR + L_TIBIA*L_TIBIA - d*d) / (2.0f*L_FEMUR*L_TIBIA);
  if (cosTibia < -1.0f || cosTibia > 1.0f) return false;
  float tibiaRad = acosf(cosTibia); // 0..pi

  float cosFemur = (L_FEMUR*L_FEMUR + d*d - L_TIBIA*L_TIBIA) / (2.0f*L_FEMUR*d);
  if (cosFemur < -1.0f || cosFemur > 1.0f) return false;
  float femurRad = acosf(cosFemur);

  // Elevación natural: ángulo de la línea r-z
  float elev = atan2f(z, r);

  // Convenciones (ajusta según montaje mecánico):
  aFemur = (elev + femurRad) * 180.0f / M_PI - 10.0f;
  aTibia = (M_PI - tibiaRad) * 180.0f / M_PI - 10.0f;

  // Ajuste de simetría si es lado derecho
  if (isRightSide) {
    aCoxa = -aCoxa;
  }

  // Limitar a rangos seguros
  if (aCoxa < COXA_MIN) aCoxa = COXA_MIN;
  if (aCoxa > COXA_MAX) aCoxa = COXA_MAX;
  if (aFemur < FEMUR_MIN) aFemur = FEMUR_MIN;
  if (aFemur > FEMUR_MAX) aFemur = FEMUR_MAX;
  if (aTibia < TIBIA_MIN) aTibia = TIBIA_MIN;
  if (aTibia > TIBIA_MAX) aTibia = TIBIA_MAX;

  // Convertir a referencia 0..180 para servo:
  // Suponemos mid (90) como neutro mecánico aproximado.
  aCoxa = 90.0f + aCoxa;
  aFemur = 90.0f + aFemur;
  aTibia = 90.0f + aTibia;

  return true;
}

// ---------- Gait: trípode ----------
// Dos trípodes: {L0, R1, L2} y {R0, L1, R2}
uint8_t tripodA[3] = {0, 4, 2}; // índices de patas L0(0), R1(4), L2(2)
uint8_t tripodB[3] = {3, 1, 5}; // R0(3), L1(1), R2(5)

// Parámetros de movimiento:
float stepHeight = 25.0f;   // mm elevación
float stepLength = 40.0f;   // mm avance
float defaultX = 80.0f;     // mm delantero
float defaultY = 50.0f;     // mm lateral (separación)
float defaultZ = -60.0f;    // mm vertical (negativo hacia abajo)
uint16_t gaitPeriodMs = 550; // velocidad (ms por semiperiodo)

// Estado de control
enum Mode {MODE_STOP, MODE_FWD, MODE_BACK, MODE_LEFT, MODE_RIGHT, MODE_STEP};
Mode mode = MODE_STOP;

// Parser de comandos
String cmdBuffer;

// Sensor VL53L0X
Adafruit_VL53L0X lox;
uint16_t obstacleThresholdMm = 200; // parar si < 200 mm
bool obstacleDetected = false;

// Temporización
uint32_t lastGaitTick = 0;

// Posiciones base de cada pata en marco del cuerpo
Vec3 basePos[6];

void initBasePos() {
  // Asumir simetría: Y positivo lado izquierdo, negativo derecho
  basePos[0] = { defaultX,  defaultY, defaultZ}; // L0
  basePos[1] = { 0.0f,      defaultY, defaultZ}; // L1
  basePos[2] = {-defaultX,  defaultY, defaultZ}; // L2
  basePos[3] = { defaultX, -defaultY, defaultZ}; // R0
  basePos[4] = { 0.0f,     -defaultY, defaultZ}; // R1
  basePos[5] = {-defaultX, -defaultY, defaultZ}; // R2
}

void applyLegAngles(uint8_t legIdx, float aCoxa, float aFemur, float aTibia) {
  const LegMap &lm = legs[legIdx];
  setServoAngle(lm.coxa, aCoxa);
  setServoAngle(lm.femur, aFemur);
  setServoAngle(lm.tibia, aTibia);
}

void moveLegTo(uint8_t legIdx, const Vec3 &p) {
  float aC,aF,aT;
  bool rightSide = (legIdx >= 3);
  if (ikSolve(p, aC, aF, aT, rightSide)) {
    applyLegAngles(legIdx, aC, aF, aT);
  }
}

// Genera objetivo por pata según fase del trípode
void computeTargets(float phase, Vec3 outTargets[6]) {
  // phase: 0..1 (semiperiodo) alterna trípodes A/B
  // movimiento según modo:
  float dx = 0, dy = 0;
  if (mode == MODE_FWD) dx = stepLength;
  else if (mode == MODE_BACK) dx = -stepLength;
  else if (mode == MODE_LEFT) dy = stepLength;
  else if (mode == MODE_RIGHT) dy = -stepLength;

  // Para cada pata, si pertenece al grupo activo en esta mitad, elevar y avanzar, si no, retroceder a ras de suelo
  auto inTripod = [&](uint8_t leg, const uint8_t group[3]) {
    return (leg == group[0] || leg == group[1] || leg == group[2]);
  };

  const uint8_t* activeGroup = ((millis() / gaitPeriodMs) % 2 == 0) ? tripodA : tripodB;

  for (uint8_t i=0; i<6; ++i) {
    Vec3 base = basePos[i];
    Vec3 p = base;
    float t = phase; // 0..1 normalizado

    if (mode == MODE_STOP) {
      // Mantener base
      // Si hay STEP, se maneja externamente
    } else {
      if (inTripod(i, activeGroup)) {
        // Fase de swing (elevar y mover hacia adelante)
        float lift = stepHeight * sinf(t * M_PI); // campana
        p.z = base.z + lift;
        p.x = base.x + dx * (t - 0.5f); // centrado
        p.y = base.y + dy * (t - 0.5f);
      } else {
        // Fase de stance (contacto, mover hacia atrás de forma complementaria)
        p.z = base.z; // suelo
        p.x = base.x - dx * (t - 0.5f);
        p.y = base.y - dy * (t - 0.5f);
      }
    }

    outTargets[i] = p;
  }
}

void allLegsToHome() {
  for (uint8_t i=0; i<6; ++i) {
    moveLegTo(i, basePos[i]);
  }
}

// ---------- Comunicación por Zigbee (Serial1, transparente) ----------
void handleSerial1() {
  while (Serial1.available()) {
    char c = (char)Serial1.read();
    if (c == '\r') continue;
    if (c == '\n') {
      processCommand(cmdBuffer);
      cmdBuffer = "";
    } else {
      if (cmdBuffer.length() < 64)
        cmdBuffer += c;
    }
  }
}

void sendStatus(const String &s) {
  Serial1.print("[HEX] ");
  Serial1.println(s);
  Serial.print("[HEX] ");
  Serial.println(s);
}

void processCommand(const String &cmd) {
  String up = cmd;
  up.trim();
  up.toUpperCase();

  if (up == "FWD") { mode = MODE_FWD; sendStatus("MODE=FWD"); }
  else if (up == "BACK") { mode = MODE_BACK; sendStatus("MODE=BACK"); }
  else if (up == "LEFT") { mode = MODE_LEFT; sendStatus("MODE=LEFT"); }
  else if (up == "RIGHT") { mode = MODE_RIGHT; sendStatus("MODE=RIGHT"); }
  else if (up == "STOP") { mode = MODE_STOP; sendStatus("MODE=STOP"); allLegsToHome(); }
  else if (up.startsWith("SPEED ")) {
    int v = up.substring(6).toInt();
    if (v >= 300 && v <= 1200) {
      gaitPeriodMs = (uint16_t)v;
      sendStatus("SPEED(ms)=" + String(gaitPeriodMs));
    } else {
      sendStatus("ERR SPEED [300..1200]");
    }
  } else if (up.startsWith("STEP")) {
    mode = MODE_STEP;
    sendStatus("MODE=STEP (single)");
  } else if (up.startsWith("CALIB ")) {
    // Formato: CALIB leg dof offset_us
    // leg=0..5, dof=0(coxa)/1(femur)/2(tibia)
    int leg=-1, dof=-1, off=0;
    int n = sscanf(up.c_str(), "CALIB %d %d %d", &leg, &dof, &off);
    if (n==3 && leg>=0 && leg<6 && dof>=0 && dof<3 && off>-500 && off<500) {
      uint8_t ch = (dof==0) ? legs[leg].coxa : (dof==1 ? legs[leg].femur : legs[leg].tibia);
      servoOffsetUs[ch] = off;
      sendStatus("CALIB OK leg="+String(leg)+" dof="+String(dof)+" off="+String(off));
      // Reaplica home con nuevo offset
      moveLegTo(leg, basePos[leg]);
    } else {
      sendStatus("ERR CALIB usage: CALIB leg(0..5) dof(0..2) offset(-499..499)");
    }
  } else {
    sendStatus("UNKNOWN CMD: " + up);
  }
}

// ---------- VL53L0X ----------
bool readObstacle(uint16_t &mm) {
  VL53L0X_RangingMeasurementData_t measure;
  lox.rangingTest(&measure, false); // no debug
  if (measure.RangeStatus == 4) { // out of range
    return false;
  } else {
    mm = measure.RangeMilliMeter;
    return true;
  }
}

// ---------- Setup ----------
void setup() {
  Serial.begin(115200);
  Serial1.begin(115200); // XBee
  delay(50);

  Wire.begin();
  Wire.setClock(400000); // I2C rápido
  pca.begin();
  pca.setPWMFreq(SERVO_FREQ);
  delay(10);

  if (!lox.begin()) {
    Serial.println("VL53L0X no detectado. Verifique conexiones.");
  } else {
    Serial.println("VL53L0X listo.");
  }

  initBasePos();
  allLegsToHome();

  Serial.println("Hexapod listo. Comandos: FWD/BACK/LEFT/RIGHT/STOP/SPEED n/STEP/CALIB leg dof off");
  sendStatus("READY");
}

// ---------- Loop ----------
void loop() {
  handleSerial1();

  // Sensado de obstáculo
  uint16_t dist;
  if (readObstacle(dist)) {
    if (dist < obstacleThresholdMm) {
      if (!obstacleDetected) {
        obstacleDetected = true;
        mode = MODE_STOP;
        sendStatus("OBSTACLE " + String(dist) + "mm -> STOP");
        allLegsToHome();
      }
    } else {
      if (obstacleDetected) {
        obstacleDetected = false;
        sendStatus("PATH CLEAR");
      }
    }
  }

  // Gait update
  uint32_t now = millis();
  if (now - lastGaitTick >= 20) { // 50 Hz update
    lastGaitTick = now;

    // Calcular fase del semiperiodo
    uint32_t t = now % gaitPeriodMs;
    float phase = (float)t / (float)gaitPeriodMs; // 0..1

    Vec3 targets[6];
    computeTargets(phase, targets);

    if (mode == MODE_STEP) {
      // Ejecuta una mitad de ciclo y luego STOP
      static bool stepStarted = false;
      static uint32_t stepStartTime = 0;
      if (!stepStarted) {
        stepStarted = true;
        stepStartTime = now;
      }
      for (uint8_t i=0; i<6; ++i) moveLegTo(i, targets[i]);
      if (now - stepStartTime >= gaitPeriodMs) {
        mode = MODE_STOP; stepStarted = false;
        allLegsToHome();
        sendStatus("STEP DONE");
      }
    } else if (mode == MODE_STOP) {
      // Mantener home
      // no-op para evitar jitter
    } else {
      for (uint8_t i=0; i<6; ++i) moveLegTo(i, targets[i]);
    }
  }
}

Breve explicación de las partes clave:
– Configuración del PCA9685 a 50 Hz (SERVO_FREQ = 50) y conversión de microsegundos a ticks (usToTicks).
– Estructura LegMap para asociar canales a cada DOF de cada pata.
– IK simplificada: separa rotación de coxa (atan2 en planta) y resuelve fémur/tibia con ley del coseno en el plano r-z.
– Gait trípode: alterna grupos A y B; una mitad eleva y avanza, la otra mitad mantiene contacto y “arrastra” en sentido contrario para el ciclo de paso.
– Parser de comandos (Serial1) para XBee en modo transparente a 115200 bps.
– VL53L0X: al detectar un obstáculo por debajo de 200 mm, detiene el movimiento y centra.
– Calibración en caliente por offset por-servo (microsegundos) con comando CALIB.

Compilación/flash/ejecución

Asegúrate de tener conectado el Arduino Mega 2560 por USB. Identifica el puerto (Linux suele ser /dev/ttyACM0 o /dev/ttyUSB0).

1) Instalar Arduino CLI 0.35.3 (Linux):

# Descarga e instalación
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=$HOME/.local/bin sh -s 0.35.3

# Verificar versión
$HOME/.local/bin/arduino-cli version
# arduino-cli Version: 0.35.3

# Añadir al PATH (opcional, si no lo tienes):
echo 'export PATH=$HOME/.local/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

2) Configurar core y bibliotecas exactas:

arduino-cli core update-index
arduino-cli core install arduino:avr@1.8.6

arduino-cli lib install "Adafruit PWM Servo Driver Library@2.4.1"
arduino-cli lib install "Adafruit BusIO@1.14.5"
arduino-cli lib install "Adafruit Unified Sensor@1.1.14"
arduino-cli lib install "Adafruit VL53L0X@1.3.1"

3) Compilar (FQBN para Mega 2560: arduino:avr:mega):

# Posicionarse en el directorio que contiene la carpeta servo_hexapod_zigbee/
arduino-cli compile --fqbn arduino:avr:mega servo_hexapod_zigbee

4) Subir al Mega 2560:

# Sustituye /dev/ttyACM0 por el puerto correcto de tu sistema
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega servo_hexapod_zigbee

5) Monitor serie (USB) para logs de depuración:

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

6) Puente Zigbee (lado PC) con minicom (para enviar comandos al robot):

# Detecta el puerto del adaptador XBee USB, por ejemplo /dev/ttyUSB0
sudo apt-get install -y minicom
minicom -b 115200 -D /dev/ttyUSB0
# Escribe comandos: FWD, BACK, LEFT, RIGHT, STOP, SPEED 600, STEP, CALIB 0 1 20, etc.

Opcional (Python 3.10.12) para transmitir comandos al XBee remoto:

# archivo: send_zigbee.py
import sys, time, serial

port = "/dev/ttyUSB0"  # Cambia según tu sistema
baud = 115200
cmd = "FWD"            # o cualquier comando

if len(sys.argv) > 1:
    cmd = " ".join(sys.argv[1:])

with serial.Serial(port, baud, timeout=1) as ser:
    ser.write((cmd + "\n").encode("ascii"))
    time.sleep(0.1)
    # Leer respuesta del robot (eco por Zigbee)
    while ser.in_waiting:
        print(ser.readline().decode(errors="ignore").rstrip())

Ejecuta:

python3 send_zigbee.py "SPEED 600"
python3 send_zigbee.py "FWD"

Validación paso a paso

1) Verificación de I2C:
– Al abrir el monitor serie (USB), el programa debe imprimir “VL53L0X listo.” si el sensor es detectado. Si no, revisa SDA/SCL y VCC/GND.
– Los servos deberían llevar cada pata a su postura base (home) sin vibraciones notables.

2) Verificación de PCA9685 y servos:
– Debes observar un movimiento suave a posiciones centradas al iniciar.
– Si alguna articulación se mueve en sentido incorrecto, ajusta servoDir[] en el código y vuelve a subir.
– Ajusta offsets con CALIB para centrar mecánicamente:
– Ejemplo: “CALIB 0 1 20” corrige +20 us el fémur de L0.

3) Verificación del XBee (modo transparente):
– Desde minicom en el XBee remoto, envía “STOP”. Debes ver un eco “[HEX] MODE=STOP” tanto en minicom como en el monitor USB del Arduino.
– Envía “SPEED 700” y luego “FWD”. El robot debe iniciar un paso de trípode hacia adelante sin bloqueos.

4) Validación de locomoción:
– Con “FWD” el robot alterna trípodes. Observa que tres patas se elevan mientras las otras tres soportan.
– Cambia a “LEFT” o “RIGHT” y confirma el giro sobre el plano (ajuste lateral con dy).

5) Evitación con VL53L0X:
– Acerca un objeto al sensor frontal. Cuando esté a menos de ~200 mm, el robot debe emitir “[HEX] OBSTACLE xxxmm -> STOP” y volver a home.
– Al retirar el obstáculo, verás “[HEX] PATH CLEAR” y podrás reanudar con “FWD”.

6) Comando “STEP”:
– Ejecuta un semiperiodo de la marcha y vuelve a STOP automáticamente con “STEP DONE”.

7) Estabilidad eléctrica:
– Durante “FWD” sostenido, verifica que la fuente de servos no cae de tensión (ni reinicios del Mega).
– Toca el PCA9685: no debe estar excesivamente caliente. Si lo está, revisa la corriente de los servos y el duty.

8) Revisión de sincronía:
– Modifica “SPEED n” y evalúa la correspondencia entre n y la rapidez del paso. Valores muy bajos (<300 ms) pueden causar movimientos bruscos.

Troubleshooting

1) Servos tiemblan o se resetea el Mega al mover:
– Causa: falta de corriente o caída de tensión en V+ o masa no común.
– Solución: usa una fuente de 6–7.4 V con suficiente amperaje; une GND de fuente y GND del Mega; emplea cables gruesos y cortos.

2) VL53L0X no detectado (“no detectado” al iniciar):
– Causa: SDA/SCL invertidos, falta de VCC, mal módulo (3.3 V-only), nivel I2C incorrecto.
– Solución: en el Mega, SDA=20 y SCL=21; verifica VCC según módulo; prueba a 3.3 V si tu breakout no es 5V tolerant; revisa soldaduras.

3) Un grupo de servos se mueve al revés (pata gira al lado opuesto):
– Causa: inversión lógica no ajustada.
– Solución: cambia el signo en servoDir[ch] correspondiente (1 a -1). Recalibra con CALIB si es necesario.

4) No hay comunicación Zigbee (no respuestas en minicom):
– Causa: PAN ID distinto, baudios no coinciden, no están en misma red (coordinator/router).
– Solución: reconfigura ambos XBee con ATID igual, ATBD7 (115200), uno con ATCE1 (coordinator, lado PC) y el del robot con ATCE0 (router). Guarda con ATWR.

5) Compilación falla por bibliotecas:
– Causa: versiones no instaladas o dependencias faltantes.
– Solución: reinstala exactamente:
– arduino-cli lib install «Adafruit PWM Servo Driver Library@2.4.1»
– arduino-cli lib install «Adafruit BusIO@1.14.5»
– arduino-cli lib install «Adafruit Unified Sensor@1.1.14»
– arduino-cli lib install «Adafruit VL53L0X@1.3.1»

6) Servos no se mueven pero log por USB es correcto:
– Causa: PCA9685 sin V+ de potencia o sin GND común.
– Solución: aplica 6–7.4 V en V+ del PCA9685, conecta GND de la fuente a GND del PCA y del Mega.

7) Movimientos violentos al iniciar:
– Causa: offsets no calibrados o límites de ángulo mal configurados.
– Solución: ajusta servoOffsetUs[] y límites (COXA/FEMUR/TIBIA min/max). Empieza con allLegsToHome() antes de gait.

8) Lecturas erráticas de ToF:
– Causa: superficie brillante, ambiente IR saturado, tiempo de integración corto.
– Solución: promedia lecturas, sube el umbral o filtra por estados. Asegúrate de no tapar el sensor.

Mejoras/variantes

  • Migrar a API mode (AP=2) en XBee:
  • Permite direccionamiento a múltiples nodos, ACK y telemetría estructurada. Usa una librería XBee para frame parsing y añade CRC/timeout.

  • Calibración avanzada:

  • Guardar offsets y parámetros en EEPROM del Mega.
  • Añadir rutina de auto-calibración por guías impresas y un láser de referencia.

  • Gaits adicionales:

  • Ripple gait (estabilidad máxima), wave gait (lento y estable), amble (suavidad).
  • Transiciones suaves entre gaits según terreno.

  • Planificación local:

  • Usar el VL53L0X para mantener distancia objetivo, no solo stop. Integrar dos VL53L0X (frontal y lateral), cambiando dirección I2C de uno con pin XSHUT.

  • Telemetría:

  • Reporte periódico de estado (modo, distancia, velocidad, tensión batería).
  • Mando Zigbee con joystick o GUI en PC.

  • Gestión de energía:

  • Añadir sensor de corriente y voltaje (INA219) para limitar esfuerzos.
  • Modo reposo automático si no hay comando en N segundos.

  • Suavizado de trayectorias:

  • Interpolación spline/coseno para cada DOF, control feedforward de velocidad y aceleración.

  • Seguridad:

  • Watchdog (WDT) en el ATmega2560, kill-switch por comando, y botón físico de emergencia en línea V+.

Checklist de verificación

  • [ ] Arduino Mega 2560 detectado por el sistema (puerto serie visible).
  • [ ] Arduino CLI 0.35.3 instalado y en PATH.
  • [ ] Core arduino:avr@1.8.6 instalado sin errores.
  • [ ] Bibliotecas instaladas con versiones: Adafruit PWM Servo Driver 2.4.1, BusIO 1.14.5, Unified Sensor 1.1.14, VL53L0X 1.3.1.
  • [ ] PCA9685 conectado a SDA(20)/SCL(21), VCC=5 V, GND común; V+ con 6–7.4 V externa.
  • [ ] VL53L0X conectado al mismo bus I2C y detectado (mensaje “VL53L0X listo.”).
  • [ ] XBee S2C en el robot conectado a Serial1 (TX1=18, RX1=19) a 115200 bps vía shield/conversor nivel.
  • [ ] XBee remoto en PC configurado con el mismo PAN ID y baudios; se reciben ecos [HEX] en minicom al enviar comandos.
  • [ ] “STOP” recentra patas; “FWD/BACK/LEFT/RIGHT” mueven con trípode; “SPEED n” ajusta ritmo (300–1200 ms).
  • [ ] Acercar un objeto < 200 mm detiene el robot con mensaje de obstáculo.
  • [ ] Offsets calibrados por pata/DOF con “CALIB leg dof off” para posturas neutras.

Con esta práctica culminada, dispones de un servo-hexápodo controlado por Zigbee con un pipeline de toolchain reproducible (Arduino CLI), sensado ToF VL53L0X e integración estable de PCA9685 para 18 servos en un Arduino Mega 2560. El proyecto es una base sólida para evolucionar hacia control cinemático más avanzado, gaits adaptativos y redes Zigbee multipunto.

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 recomendado para el proyecto?




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




Pregunta 3: ¿Qué hardware se utiliza como driver de 16 canales servo?




Pregunta 4: ¿Cuál es la dirección por defecto del PCA9685?




Pregunta 5: ¿Qué tipo de sensor es el VL53L0X?




Pregunta 6: ¿Qué tipo de conversor se necesita para conectar el XBee S2C al Arduino Mega 2560?




Pregunta 7: ¿Qué biblioteca de Arduino se necesita para controlar el PCA9685?




Pregunta 8: ¿Qué módulo se utiliza para la comunicación Zigbee?




Pregunta 9: ¿Cuál es la opción de terminal serie recomendada para Linux?




Pregunta 10: ¿Cuántos servos se utilizan en el hexápodo del proyecto?




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

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

Sígueme:


Caso práctico: 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:


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:


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:


Caso práctico: Gamepad BLE de gestos con Arduino Nano 33 BLE

Caso práctico: Gamepad BLE de gestos con Arduino Nano 33 BLE — hero

Objetivo y caso de uso

Qué construirás: Un gamepad de gestos BLE utilizando Arduino Nano 33 BLE, APDS9960 y MPU6050 para transmitir estados mediante gestos y inclinaciones.

Para qué sirve

  • Control de videojuegos mediante gestos, permitiendo una experiencia de juego más inmersiva.
  • Interacción con dispositivos IoT a través de comandos gestuales, como encender luces o controlar dispositivos multimedia.
  • Desarrollo de aplicaciones de accesibilidad que permiten a usuarios con movilidad reducida interactuar con tecnología mediante gestos.

Resultado esperado

  • Transmisión de datos a través de BLE con una latencia inferior a 20 ms.
  • Reconocimiento de gestos con una tasa de éxito del 95% en condiciones normales de uso.
  • Capacidad de enviar hasta 10 comandos por segundo sin pérdida de datos.

Público objetivo: Desarrolladores de hardware y software; Nivel: Avanzado

Arquitectura/flujo: Sensor APDS9960 capta gestos, MPU6050 detecta inclinaciones, Arduino Nano 33 BLE procesa y transmite datos vía BLE.

Nivel: Avanzado

Prerrequisitos

Sistema operativo soportado (verificado)

  • Linux:
  • Ubuntu 22.04 LTS o 24.04 LTS (64-bit)
  • Kernel con soporte udev para puertos ACM/ttyUSB (estándar)
  • Windows:
  • Windows 11 (22H2/23H2, 64-bit)
  • Windows 10 (22H2, 64-bit)
  • macOS:
  • macOS 13 Ventura o macOS 14 Sonoma (Apple Silicon o Intel)

Toolchain exacto empleado (versiones)

  • Arduino CLI: 0.35.3
  • Núcleo/Board core:
  • arduino:mbed_nano (Arduino Mbed OS Nano Boards): 4.1.3
  • Bibliotecas Arduino (instaladas desde Library Manager con arduino-cli):
  • ArduinoBLE: 1.3.6
  • SparkFun APDS9960 RGB and Gesture Sensor: 1.4.3
  • Adafruit MPU6050: 2.2.5
  • Adafruit Unified Sensor: 1.1.14
  • Adafruit BusIO: 1.16.1
  • Python (opcional para validación y mapeo a teclado/joystick virtual en el host):
  • Python 3.11.x
  • Paquetes pip (validación BLE y mapeo):
    • bleak: 0.22.2
    • evdev: 1.6.1 (Linux para gamepad/teclado virtual via uinput)
    • pynput: 1.7.6 (alternativa multiplataforma para emular teclado)
  • Linux: asegurar permisos para uinput/evdev (grupo input o reglas udev)

Nota sobre drivers:
– Arduino Nano 33 BLE aparece normalmente como /dev/ttyACM0 (Linux), COMx (Windows) o /dev/cu.usbmodemXXXX (macOS) y usa driver CDC (no requiere CP210x/CH34x).
– Bluetooth Low Energy: el host (PC/teléfono) debe soportar BLE 4.2+ para conectarse al periférico.

Materiales

  • 1x Arduino Nano 33 BLE (nRF52840, 3.3 V lógicos y alimentación desde USB)
  • 1x Sensor de gestos y color APDS9960 (placa breakout a 3.3 V, p. ej., SparkFun SEN-12787 o equivalente 3V3)
  • 1x Acelerómetro/Giróscopo MPU6050 (placa breakout 3.3 V o 5 V con conversión de nivel; preferir versión 3.3 V nativa para Nano 33 BLE)
  • Cables Dupont macho–macho para I2C y señales de interrupción (x10 aprox.)
  • Protoboard
  • PC con Bluetooth 4.2+ y USB para programar el Nano 33 BLE
  • Opcional:
  • Smartphone con app “nRF Connect for Mobile” (Android/iOS) para depurar BLE.
  • Resistencia pull-up si tu breakout no la incluye (muchos módulos I2C ya tienen pull-ups en SDA/SCL).

Modelo exacto respetado en todo el caso práctico:
– Arduino Nano 33 BLE + APDS9960 + MPU6050

Preparación y conexión

Instalación de Arduino CLI y núcleo de la placa

  1. Instala Arduino CLI (si no lo tienes):
  2. Linux/macOS (con Homebrew en macOS opcional) o descarga desde releases oficiales. Aquí usamos una instalación directa binaria o paquete del sistema.
  3. Actualiza el índice de cores:
    arduino-cli core update-index
  4. Instala el core de la familia Nano 33 BLE:
    arduino-cli core install arduino:mbed_nano@4.1.3
  5. Verifica que el FQBN esté disponible:
    arduino-cli board listall | grep nano33ble -i
    Debe listar: arduino:mbed_nano:nano33ble

Instalación de bibliotecas requeridas (versiones fijas)

Ejecuta:

arduino-cli lib install "ArduinoBLE@1.3.6"
arduino-cli lib install "SparkFun APDS9960 RGB and Gesture Sensor@1.4.3"
arduino-cli lib install "Adafruit MPU6050@2.2.5"
arduino-cli lib install "Adafruit Unified Sensor@1.1.14"
arduino-cli lib install "Adafruit BusIO@1.16.1"

Cableado y pines

Usaremos el bus I2C del Nano 33 BLE (3.3 V lógicos). En el Nano 33 BLE, los pines I2C están serigrafiados como SDA y SCL cerca del conector. Evita usar 5 V en las señales; alimenta los módulos a 3V3.

  • Señales obligatorias: SDA, SCL, GND, 3V3
  • Señales opcionales para interrupciones: INT_APDS (APDS9960) e INT_MPU (MPU6050) para latencias menores; el ejemplo funcionará también sin INT, haciendo polling suave.

Tabla de conexión recomendada:

Módulo Señal Pin en módulo Pin en Nano 33 BLE Notas
APDS9960 VCC VCC (3.3 V) 3V3 3.3 V únicamente
APDS9960 GND GND GND Común
APDS9960 SDA SDA SDA I2C
APDS9960 SCL SCL SCL I2C
APDS9960 INT (opcional) INT D2 Interrupción de gesto
MPU6050 VCC VCC (3.3 V preferido) 3V3 Si el módulo es 5 V, confirme conversión de nivel
MPU6050 GND GND GND Común
MPU6050 SDA SDA SDA I2C
MPU6050 SCL SCL SCL I2C
MPU6050 AD0 (addr) AD0 GND (o 3V3) GND→0x68, 3V3→0x69
MPU6050 INT (opcional) INT D3 Interrupción de datos listos
Nano 33 BLE LED LED_BUILTIN Indicador estado BLE

Direcciones I2C por defecto:
– APDS9960: 0x39
– MPU6050: 0x68 (AD0 a GND) o 0x69 (AD0 a 3V3)

Verificación rápida (opcional) con i2cdetect (Linux, si dispones de adaptador I2C USB):
– No es necesaria si cableas al Nano 33 BLE, pero útil cuando depuras módulos fuera de la placa.

Notas de montaje

  • Mantén el APDS9960 con el sensor óptico orientado hacia el exterior (gestos “up/down/left/right” dependen de la orientación).
  • Fija el MPU6050 firme y define una “posición neutra” plana; esto ayuda a calibrar offset de pitch/roll.

Código completo (firmware Arduino + script de validación opcional)

A continuación se presenta el firmware completo en C++ para Arduino Nano 33 BLE. Implementa:
– Lectura y filtrado de pitch/roll desde MPU6050.
– Lectura de gestos desde APDS9960 (arriba/abajo/izquierda/derecha).
– Publicación por BLE en un servicio personalizado tipo “ble-gesture-gamepad” con un characteristic binario (notificaciones) que empaqueta ejes y botones.

El paquete enviado (6 bytes) tiene el siguiente layout:
– Byte 0-1: buttons (uint16_t, bits de botones)
– Byte 2: axisX (int8_t, -127..127) — derivado de roll
– Byte 3: axisY (int8_t, -127..127) — derivado de pitch
– Byte 4: dpad (uint8_t, 0x0 parada, 0x1 arriba, 0x2 derecha, 0x3 abajo, 0x4 izquierda, etc.)
– Byte 5: flags (uint8_t, reservado para estados: 0x01 calibrado, 0x02 gesto_activo, etc.)

Mapeo propuesto:
– Gestos APDS9960:
– UP → botón 1 (bit 0) y dpad=UP
– DOWN → botón 2 (bit 1) y dpad=DOWN
– LEFT → botón 3 (bit 2) y dpad=LEFT
– RIGHT → botón 4 (bit 3) y dpad=RIGHT
– Tilt (MPU6050):
– roll → eje X
– pitch → eje Y
– Deadzone configurable

Firmware Arduino (C++)

Guarda el sketch en una carpeta llamada ble-gesture-gamepad para usar con arduino-cli.

/*
  Proyecto: ble-gesture-gamepad
  Dispositivo: Arduino Nano 33 BLE + APDS9960 + MPU6050
  Toolchain:
    - Arduino CLI 0.35.3
    - Core arduino:mbed_nano@4.1.3
    - Libs: ArduinoBLE 1.3.6, SparkFun APDS9960 1.4.3, Adafruit MPU6050 2.2.5, Adafruit Unified Sensor 1.1.14, Adafruit BusIO 1.16.1

  BLE Service UUID (custom): 19B10000-E8F2-537E-4F6C-D104768A1214
  BLE Characteristic UUID:   19B10001-E8F2-537E-4F6C-D104768A1214
*/

#include <Arduino.h>
#include <Wire.h>
#include <ArduinoBLE.h>
#include <SparkFun_APDS9960.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>

// Pines opcionales de interrupción (ajusta si conectaste INT)
constexpr int PIN_INT_APDS = 2;
constexpr int PIN_INT_MPU  = 3;

// I2C addresses
constexpr uint8_t I2C_ADDR_APDS = 0x39;
constexpr uint8_t I2C_ADDR_MPU  = 0x68; // cambia a 0x69 si AD0=3V3

// BLE custom service/characteristic
BLEService gamepadService("19B10000-E8F2-537E-4F6C-D104768A1214");
BLECharacteristic gpChar("19B10001-E8F2-537E-4F6C-D104768A1214",  // data packet
                         BLERead | BLENotify, 6, true); // 6 bytes, fixed length

// Sensores
SparkFun_APDS9960 apds;
Adafruit_MPU6050 mpu;

// Estado y filtros
float pitchOffset = 0.0f, rollOffset = 0.0f;
bool calibrated = false;
unsigned long calibStartMs = 0;
const unsigned long calibDurationMs = 1500; // toma offset inicial 1.5 s
const float alphaLPF = 0.2f; // filtro exponencial para ejes

float filtRoll = 0.0f;
float filtPitch = 0.0f;

uint16_t buttons = 0;
uint8_t dpad = 0; // 0 none, 1 up, 2 right, 3 down, 4 left
uint8_t flags = 0;

// Config ejes
const float maxAngle = 35.0f;     // saturación para mapeo a -127..127
const int8_t deadzone = 6;        // zona muerta en unidades -127..127
const uint16_t gestureHoldMs = 180; // mantener botón por gesto por ~180ms

// Control de tiempos
unsigned long lastSend = 0;
const uint16_t sendPeriodMs = 20; // 50 Hz

// Gestión de gestos con "hold"
unsigned long gestureExpireMs = 0;

static inline int8_t fmapAngleToAxis(float angleDeg) {
  // saturación
  if (angleDeg >  maxAngle) angleDeg =  maxAngle;
  if (angleDeg < -maxAngle) angleDeg = -maxAngle;
  // map [-maxAngle..maxAngle] a [-127..127]
  float val = (angleDeg / maxAngle) * 127.0f;
  int v = (int)roundf(val);
  if (v >= -deadzone && v <= deadzone) v = 0;
  if (v > 127) v = 127;
  if (v < -127) v = -127;
  return (int8_t)v;
}

void setButton(int idx, bool pressed) {
  if (idx < 0 || idx > 15) return;
  if (pressed) buttons |= (1u << idx);
  else buttons &= ~(1u << idx);
}

void applyGesture(uint8_t g) {
  // Reset dpad; set specific
  dpad = 0;
  switch (g) {
    case DIR_UP:
      setButton(0, true); dpad = 1; break;
    case DIR_RIGHT:
      setButton(3, true); dpad = 2; break;
    case DIR_DOWN:
      setButton(1, true); dpad = 3; break;
    case DIR_LEFT:
      setButton(2, true); dpad = 4; break;
    default:
      break;
  }
  if (g == DIR_UP || g == DIR_DOWN || g == DIR_LEFT || g == DIR_RIGHT) {
    flags |= 0x02; // gesto activo
    gestureExpireMs = millis() + gestureHoldMs;
  }
}

void clearGestureHoldIfExpired() {
  if (gestureExpireMs != 0 && millis() > gestureExpireMs) {
    // liberar botones 0..3
    setButton(0, false);
    setButton(1, false);
    setButton(2, false);
    setButton(3, false);
    dpad = 0;
    flags &= ~0x02;
    gestureExpireMs = 0;
  }
}

bool initAPDS() {
  if (!apds.init()) {
    return false;
  }
  // Solo gesto para este proyecto (podrías habilitar Prox/Color si quieres)
  if (!apds.enableGestureSensor(true)) {
    return false;
  }
  return true;
}

bool initMPU() {
  if (!mpu.begin(I2C_ADDR_MPU, &Wire)) {
    return false;
  }
  mpu.setAccelerometerRange(MPU6050_RANGE_4_G);
  mpu.setGyroRange(MPU6050_RANGE_500_DEG);
  mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
  delay(100);
  return true;
}

void calibrateOffsets() {
  // promedia pitch/roll iniciales durante calibDurationMs
  const unsigned int N = 60;
  float sumPitch = 0.0f, sumRoll = 0.0f;
  for (unsigned int i = 0; i < N; ++i) {
    sensors_event_t a, g, temp;
    mpu.getEvent(&a, &g, &temp);
    float ax = a.acceleration.x;
    float ay = a.acceleration.y;
    float az = a.acceleration.z;
    // convención pitch/roll con acelerómetro
    float roll = atan2f(ay, az) * 180.0f / PI;
    float pitch = atan2f(-ax, sqrtf(ay * ay + az * az)) * 180.0f / PI;
    sumPitch += pitch;
    sumRoll += roll;
    delay(10);
  }
  pitchOffset = sumPitch / N;
  rollOffset = sumRoll / N;
  filtPitch = 0.0f;
  filtRoll = 0.0f;
  calibrated = true;
  flags |= 0x01;
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);

  Serial.begin(115200);
  while (!Serial && millis() < 3000) { /* opcional espera serial */ }

  Wire.begin();

  if (!initAPDS()) {
    Serial.println("Error: APDS9960 no inicializado.");
  } else {
    Serial.println("APDS9960 OK (gestures on).");
  }

  if (!initMPU()) {
    Serial.println("Error: MPU6050 no inicializado.");
  } else {
    Serial.println("MPU6050 OK.");
  }

  if (!BLE.begin()) {
    Serial.println("Error: BLE no inicializado.");
    while (1) { delay(1000); }
  }

  BLE.setLocalName("BLEGesturePad");
  BLE.setDeviceName("BLEGesturePad");
  BLE.setAdvertisedService(gamepadService);
  gamepadService.addCharacteristic(gpChar);
  BLE.addService(gamepadService);

  // Inicializa paquete a ceros
  uint8_t pkt[6] = {0};
  gpChar.writeValue(pkt, sizeof(pkt));

  BLE.advertise();
  Serial.println("BLE advertising como 'BLEGesturePad'.");

  calibStartMs = millis();
}

void loop() {
  // Manejo de centrado/calibración automática al inicio
  if (!calibrated && millis() - calibStartMs > 400) {
    calibrateOffsets();
    Serial.print("Calibrado offsets: pitch="); Serial.print(pitchOffset);
    Serial.print(", roll="); Serial.println(rollOffset);
  }

  BLEDevice central = BLE.central();
  if (central && central.connected()) {
    digitalWrite(LED_BUILTIN, HIGH);

    // bucle activo mientras conectado
    while (central.connected()) {
      // Lectura sensores
      sensors_event_t a, g, temp;
      mpu.getEvent(&a, &g, &temp);

      float ax = a.acceleration.x;
      float ay = a.acceleration.y;
      float az = a.acceleration.z;

      float roll = atan2f(ay, az) * 180.0f / PI - rollOffset;
      float pitch = atan2f(-ax, sqrtf(ay * ay + az * az)) * 180.0f / PI - pitchOffset;

      // Filtro exponencial (LPF)
      filtRoll = (1.0f - alphaLPF) * filtRoll + alphaLPF * roll;
      filtPitch = (1.0f - alphaLPF) * filtPitch + alphaLPF * pitch;

      // Gestos APDS (polling)
      if (apds.isGestureAvailable()) {
        int g = apds.readGesture();
        applyGesture((uint8_t)g);
        Serial.print("Gesto="); Serial.println(g);
      }
      clearGestureHoldIfExpired();

      // Mapeo a ejes
      int8_t axisX = fmapAngleToAxis(filtRoll);
      int8_t axisY = fmapAngleToAxis(filtPitch);

      // Empaquetado (6 bytes)
      uint8_t pkt[6];
      pkt[0] = (uint8_t)(buttons & 0xFF);
      pkt[1] = (uint8_t)((buttons >> 8) & 0xFF);
      pkt[2] = (uint8_t)axisX;
      pkt[3] = (uint8_t)axisY;
      pkt[4] = dpad;
      pkt[5] = flags;

      unsigned long now = millis();
      if (now - lastSend >= sendPeriodMs) {
        gpChar.writeValue(pkt, sizeof(pkt));
        lastSend = now;

        // Diagnóstico opcional
        // Serial.print("X="); Serial.print((int)axisX);
        // Serial.print(" Y="); Serial.print((int)axisY);
        // Serial.print(" B="); Serial.print(buttons, BIN);
        // Serial.print(" D="); Serial.print(dpad);
        // Serial.print(" F="); Serial.println(flags, BIN);
      }
      delay(1);
    }

    digitalWrite(LED_BUILTIN, LOW);
    // Limpia estados al desconectar
    buttons = 0;
    dpad = 0;
    flags &= ~0x02;
  } else {
    // Advertising idle
    delay(50);
  }
}

Puntos clave del firmware:
– Servicio BLE personalizado y characteristic con notificaciones: permite que cualquier cliente BLE (PC/móvil) se suscriba y reciba datos tipo “gamepad”.
– Filtro LPF sobre pitch/roll para suavizar el eje y evitar jitter.
– Calibración automática inicial (offsets de pitch/roll).
– Gestión de “hold” para gestos: cuando se detecta un gesto, el botón asociado se mantiene unos 180 ms para facilitar su captura en el host.
– Envío a 50 Hz (20 ms) para equilibrio entre latencia y consumo.

Script de validación (Python + Bleak)

Este script se conecta al periférico BLE y muestra los paquetes del gamepad. En Linux, también se incluye un ejemplo opcional para emular teclas con pynput o crear eventos con evdev (requiere permisos). Esto es útil para validar el flujo end-to-end “ble-gesture-gamepad”.

Guárdalo como host_validate.py.

#!/usr/bin/env python3
# Validador/puente simple para "ble-gesture-gamepad"
# Requisitos:
#   - Python 3.11
#   - bleak==0.22.2
#   - pynput==1.7.6 (opcional para mapear a teclado)
#   - evdev==1.6.1 (Linux opcional para uinput/eventos)
#
# Este script:
#   1) Escanea un periférico llamado "BLEGesturePad"
#   2) Se conecta y se suscribe al characteristic 19B10001-...
#   3) Muestra paquetes decodificados y, opcionalmente, simula teclas

import asyncio
from bleak import BleakScanner, BleakClient

SERVICE_UUID = "19B10000-E8F2-537E-4F6C-D104768A1214".lower()
CHAR_UUID    = "19B10001-E8F2-537E-4F6C-D104768A1214".lower()
TARGET_NAME  = "BLEGesturePad"

# Opcional: mapeo a teclado con pynput
ENABLE_KEYBOARD = False
try:
    if ENABLE_KEYBOARD:
        from pynput.keyboard import Controller, Key
        kb = Controller()
except Exception:
    ENABLE_KEYBOARD = False

def decode_packet(data: bytearray):
    if len(data) != 6:
        return None
    buttons = data[0] | (data[1] << 8)
    axisX = int.from_bytes(data[2:3], byteorder="little", signed=True)
    axisY = int.from_bytes(data[3:4], byteorder="little", signed=True)
    dpad  = data[4]
    flags = data[5]
    return buttons, axisX, axisY, dpad, flags

def handle_dpad_keyboard(dpad):
    if not ENABLE_KEYBOARD:
        return
    # Enviar toques cortos según dpad (muy simple)
    key_map = {1: 'w', 3: 's', 4: 'a', 2: 'd'}
    if dpad in key_map:
        kb.press(key_map[dpad]); kb.release(key_map[dpad])

async def main():
    print("Buscando periférico:", TARGET_NAME)
    device = None
    devices = await BleakScanner.discover(timeout=5.0)
    for d in devices:
        if d.name == TARGET_NAME:
            device = d
            break
    if device is None:
        print("No se encontró el periférico. Asegúrate de que está anunciando.")
        return

    print("Conectando a", device.address)
    async with BleakClient(device) as client:
        if not client.is_connected:
            print("No se pudo conectar.")
            return
        print("Conectado.")

        svcs = await client.get_services()
        if SERVICE_UUID not in [s.uuid.lower() for s in svcs]:
            print("Advertencia: el servicio esperado no aparece en la lista (puede ser limitación del host).")

        def notification_handler(_, data: bytearray):
            decoded = decode_packet(data)
            if decoded is None:
                print("Paquete inválido:", data.hex())
                return
            buttons, axisX, axisY, dpad, flags = decoded
            print(f"B={buttons:016b} X={axisX:+4d} Y={axisY:+4d} D={dpad} F={flags:08b}")
            handle_dpad_keyboard(dpad)

        await client.start_notify(CHAR_UUID, notification_handler)
        print("Suscrito a notificaciones. Mueve el Nano o realiza gestos delante del APDS9960.")

        try:
            while True:
                await asyncio.sleep(0.5)
        except KeyboardInterrupt:
            pass

        await client.stop_notify(CHAR_UUID)
        print("Desconectado.")

if __name__ == "__main__":
    asyncio.run(main())

Notas del script:
– En Windows/macOS, bleak funciona bien para suscribirse a notificaciones.
– En Linux, si quieres crear un gamepad virtual real, deberás usar uinput/evdev y dotarte de permisos; el ejemplo deja eso como opcional para no complicar el flujo principal.
– El script identifica al periférico por nombre “BLEGesturePad” y se suscribe al characteristic del servicio personalizado.

Compilación/flash/ejecución: comandos exactos y ordenados

1) Identificar el puerto serie del Nano 33 BLE:
– Conecta la placa por USB y ejecuta:
arduino-cli board list
Ejemplo de salida (Linux):
– Port: /dev/ttyACM0
– FQBN: arduino:mbed_nano:nano33ble

2) Compilar el sketch:
– Asumiendo que el directorio actual contiene la carpeta del proyecto “ble-gesture-gamepad” con el .ino:
arduino-cli compile --fqbn arduino:mbed_nano:nano33ble ble-gesture-gamepad

3) Subir el firmware:
– Linux/macOS:
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:mbed_nano:nano33ble ble-gesture-gamepad
– Windows (ajusta COMx, por ejemplo COM5):
arduino-cli upload -p COM5 --fqbn arduino:mbed_nano:nano33ble ble-gesture-gamepad

4) Monitor serial (opcional, para diagnóstico):
– Linux/macOS:
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200
– Windows:
arduino-cli monitor -p COM5 -c baudrate=115200

5) Validación BLE con smartphone (nRF Connect):
– Abre nRF Connect, escanea dispositivos, ubica “BLEGesturePad”.
– Conéctate y localiza el servicio con UUID 19B10000-… y su characteristic 19B10001-….
– Activa “Notify” y verifica la llegada de paquetes (verás datos binarios).

6) Validación BLE con PC (script Python):
– Instalar dependencias:
python3 -m pip install bleak==0.22.2 pynput==1.7.6 evdev==1.6.1
Nota: en Windows/macOS, evdev puede no instalarse (solo Linux); es opcional.
– Ejecutar el validador:
python3 host_validate.py

Validación paso a paso

1) Verificación de sensores:
– Conecta el monitor serie:
– Debes ver “APDS9960 OK (gestures on).” y “MPU6050 OK.” si todo inicializó bien.
– Si una línea reporta error, consulta “Troubleshooting”.

2) Verificación de advertising BLE:
– LED BUILTIN parpadea apagado/encendido breve cuando conectas; en el código se enciende LED cuando hay conexión BLE.
– Con nRF Connect o el script Python, busca “BLEGesturePad”:
– Si aparece, el advertising funciona.

3) Conexión y notificaciones:
– Conéctate al dispositivo y suscríbete al characteristic 19B10001-…:
– Debes recibir notificaciones a ~50 Hz cuando el host está conectado.
– Con el script Python verás líneas como:
– “B=0000000000000000 X= +0 Y= +0 D=0 F=00000001”
– Al detectar un gesto (por ejemplo UP), deberías ver B con bit0=1 y D=1 durante ~180 ms.

4) Validación de gestos con el APDS9960:
– Realiza un gesto “arriba” delante del sensor:
– El script mostrará D=1 y el bit 0 del campo B=1 durante un instante.
– Gesto “abajo”: D=3, bit1=1; “izquierda”: D=4, bit2=1; “derecha”: D=2, bit3=1.

5) Validación de ejes con el MPU6050:
– Coloca el conjunto en posición plana y deja que calibre (1.5 s aprox.; F muestra el bit 0x01 activado).
– Inclina suavemente izquierda/derecha (roll):
– X variará desde 0 hacia ±127.
– Inclina adelante/atrás (pitch):
– Y variará desde 0 hacia ±127.
– Comprobar deadzone:
– Pequeñas oscilaciones alrededor de 0 no deberían generar movimiento (eje=0).

6) Validación de flujo completo como “ble-gesture-gamepad”:
– Con el script Python, activa ENABLE_KEYBOARD=True en host_validate.py si quieres que el D-Pad simule WASD (pulsos cortos).
– Abre un juego o ventana de prueba de input y confirma que:
– Gestos UP/DOWN/LEFT/RIGHT generan WASD.
– El tilt (ejes) se ve en la consola (o puedes ampliarlo para generar flechas o joystick virtual en Linux con evdev/uinput).

7) Latencia y estabilidad:
– Observa que los eventos de gesto aparecen con retardo mínimo (<1–2 ciclos).
– Ajusta sendPeriodMs y alphaLPF si quieres mayor suavizado o menor latencia.

Troubleshooting

1) No aparece “BLEGesturePad” en el escaneo BLE:
– Causa probable:
– BLE no inició correctamente o el core no corresponde.
– Solución:
– Verifica BLE.begin() en el monitor serie.
– Reinstala core: arduino-cli core install arduino:mbed_nano@4.1.3
– Asegura que el sketch ejecuta BLE.advertise() en setup() y no hay bucles bloqueantes.

2) Error de compilación por bibliotecas no encontradas:
– Causa:
– Falta instalar librerías o versiones incompatibles.
– Solución:
– Ejecuta:
arduino-cli lib install "ArduinoBLE@1.3.6"
arduino-cli lib install "SparkFun APDS9960 RGB and Gesture Sensor@1.4.3"
arduino-cli lib install "Adafruit MPU6050@2.2.5"
arduino-cli lib install "Adafruit Unified Sensor@1.1.14"
arduino-cli lib install "Adafruit BusIO@1.16.1"

– Limpia la caché de compilación: borra el directorio build si fuera necesario.

3) APDS9960 no detecta gestos:
– Causas:
– Orientación del sensor incorrecta, iluminación ambiental extrema, distancia/gesto inapropiados.
– Conexión INT no usada (puede afectar si tuvieras otra configuración).
– Solución:
– Asegura VCC=3V3, GND común, SDA/SCL correctos.
– Si hay pull-ups extra en el breakout, evita conflictos con otros módulos.
– Prueba gestos a 5–10 cm con movimientos claros y consistentes.
– Cambia el ángulo del sensor de modo que el fotodiodo “mire” hacia tu mano.

4) MPU6050 devuelve lecturas erráticas o saturadas:
– Causas:
– Alimentación a 5 V en placas sin conversión de nivel (no apto para Nano 33 BLE).
– Módulo con AD0 en 3V3 (dirección 0x69) pero código a 0x68.
– Solución:
– Usa 3V3 de la placa para VCC y asegúrate de que el breakout es 3.3 V compatible.
– Ajusta I2C_ADDR_MPU a 0x69 si AD0 está a 3V3.

5) No puedo subir el sketch (upload) o el puerto no aparece:
– Causas:
– Cable USB solo carga (sin datos), puerto ocupado por otra app, permisos en Linux.
– Solución:
– Usa un cable USB de datos.
– Cierra IDEs/monitores serie.
– En Linux: añade tu usuario al grupo dialout y reconecta:
sudo usermod -a -G dialout $USER
# cierra sesión y vuelve a entrar

6) El host BLE se conecta pero no recibe notificaciones:
– Causas:
– El characteristic no se ha suscrito o no tiene propiedad Notify configurada.
– Solución:
– En nRF Connect, pulsa “Notify”.
– Verifica en el código que gpChar tiene BLENotify y se llama a writeValue en loop.

7) Pérdidas de paquetes o latencia elevada:
– Causas:
– Interferencias BLE, tasa de envío demasiado alta, filtro muy agresivo.
– Solución:
– Ajusta sendPeriodMs a 10–30 ms.
– Reduce alphaLPF si notas “lag”.
– Aleja el dispositivo de routers/USB 3.0 ruidosos.

8) En Windows, el sistema no expone el dispositivo como “Gamepad HID”:
– Causa:
– Este proyecto usa un servicio BLE personalizado, no el perfil HID GATT nativo.
– Solución:
– Usa el script Python para traducir a entradas de teclado/joystick (en Linux con evdev puedes crear un dispositivo virtual).
– Como mejora, implementa BLE HID sobre el Nano 33 BLE con una librería HID específica para mbed_nano (ver sección “Mejoras”).

Mejoras/variantes

  • Perfil BLE HID nativo:
  • Implementar el servicio HID GATT con report descriptors para que el host lo reconozca como “Gamepad” sin software intermedio. Requiere librería HID para Arduino mbed (no incluida en ArduinoBLE por defecto) o integrar un stack HID específico.
  • Vibración/háptica:
  • Añadir un motor ERM o LRA y controlar feedback desde el host mediante otra characteristic (write).
  • Optimización de latencia:
  • Reducir sendPeriodMs a 10–15 ms, ajustar parámetros de conexión BLE (intervalo de conexión) si la librería lo permite.
  • Calibración avanzada:
  • Implementar calibración por pulsación (doble reset o botón externo) y guardado de offsets en NVM.
  • Filtrado y fusión de sensores:
  • Usar un filtro complementario/kalman para combinar acelerómetro y giroscopio del MPU6050 y obtener orientación más estable.
  • Mapas de control configurables:
  • Añadir characteristic de configuración para remapear botones y sensibilidad desde una app host.
  • Alimentación y portabilidad:
  • Alimentación por batería LiPo + módulo de carga, diseñando un “gamepad” portátil por gestos.
  • Compatibilidad con juegos:
  • En Linux, crear un dispositivo “uinput” de tipo gamepad (evdev) para que el sistema lo vea como mando real. En Windows, usar ViGEmBus con librerías Python para exponer un XInput virtual.

Checklist de verificación

  • [ ] Tengo Arduino CLI 0.35.3 instalado y en PATH.
  • [ ] Instalé el core arduino:mbed_nano@4.1.3 con arduino-cli core install.
  • [ ] Instalé las librerías exactas: ArduinoBLE 1.3.6, SparkFun APDS9960 1.4.3, Adafruit MPU6050 2.2.5, Adafruit Unified Sensor 1.1.14, Adafruit BusIO 1.16.1.
  • [ ] He cableado APDS9960 y MPU6050 a SDA/SCL y 3V3/GND del Nano 33 BLE (sin 5 V).
  • [ ] Confirmé la dirección I2C del MPU6050 (0x68 u 0x69) y ajusté el código si fue necesario.
  • [ ] El sketch compila sin errores con:
  • arduino-cli compile –fqbn arduino:mbed_nano:nano33ble ble-gesture-gamepad
  • [ ] El sketch sube correctamente al puerto de mi placa.
  • [ ] El host detecta el periférico BLE “BLEGesturePad” y puede suscribirse al characteristic 19B10001-….
  • [ ] Al mover el dispositivo, veo cambios en X/Y; al hacer gestos UP/DOWN/LEFT/RIGHT, veo el D-Pad y los bits de botones activarse brevemente.
  • [ ] He validado con nRF Connect o con host_validate.py que las notificaciones llegan a ~50 Hz.
  • [ ] (Opcional) He probado el mapeo a teclado con pynput o he investigado evdev/uinput para un gamepad virtual en Linux.
  • [ ] El proyecto cumple el objetivo “ble-gesture-gamepad”: eje analógico desde inclinación (MPU6050) y botones/D-Pad por gestos (APDS9960) transmitidos por BLE.

Con esto, dispones de un “ble-gesture-gamepad” coherente con el modelo “Arduino Nano 33 BLE + APDS9960 + MPU6050”, reproducible con Arduino CLI y validable en PC o móvil. La arquitectura es extensible para integrar BLE HID nativo, telemetría, configuración remota y mejora de algoritmos de fusión sensorial.

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 de los siguientes sistemas operativos es compatible con Arduino Nano 33 BLE?




Pregunta 2: ¿Qué versión de Arduino CLI se requiere según el artículo?




Pregunta 3: ¿Cuál es la biblioteca necesaria para el sensor de gestos y color APDS9960?




Pregunta 4: ¿Qué versión de Python se sugiere para la validación y mapeo?




Pregunta 5: ¿Qué tipo de conexión debe soportar el host para conectarse al periférico?




Pregunta 6: ¿Qué driver utiliza el Arduino Nano 33 BLE?




Pregunta 7: ¿Cuál es la versión del núcleo/board core para Arduino Mbed OS Nano Boards?




Pregunta 8: ¿Qué tipo de cables se necesita para las conexiones I2C?




Pregunta 9: ¿Qué biblioteca se utiliza para el acelerómetro/giróscopo MPU6050?




Pregunta 10: ¿Cuál es el requerimiento de kernel para Linux según el artículo?




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

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

Sígueme:


Caso práctico: Modbus Logger Arduino Mega, W5500 y MAX485

Caso práctico: Modbus Logger Arduino Mega, W5500 y MAX485 — hero

Objetivo y caso de uso

Qué construirás: Un registrador de energía Modbus que captura datos de dispositivos y los expone a través de HTTP utilizando Arduino Mega 2560, W5500 y MAX485.

Para qué sirve

  • Monitoreo en tiempo real de consumo energético en instalaciones industriales.
  • Integración de datos de sensores de energía en sistemas de gestión de edificios.
  • Registro de datos históricos para análisis de eficiencia energética.
  • Comunicación con dispositivos Modbus RTU a través de RS-485.

Resultado esperado

  • Captura de datos de consumo energético con una frecuencia de 1 segundo.
  • Exposición de datos a través de HTTP con un tiempo de respuesta menor a 200 ms.
  • Transmisión de datos Modbus RTU con una latencia inferior a 10 ms.
  • Registro de hasta 1000 entradas en microSD sin pérdida de datos.

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

Arquitectura/flujo: Arduino Mega 2560 con W5500 y MAX485, capturando datos de sensores Modbus y enviándolos a un servidor HTTP.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y versiones probadas

  • Windows 11 (23H2)
  • Ubuntu 22.04 LTS
  • macOS 14 (Sonoma)

En los tres sistemas se ha validado la compilación y carga usando Arduino CLI, sin entorno gráfico.

Toolchain exacta y versiones

  • Arduino CLI: 0.35.3
  • Core de placas AVR: arduino:avr@1.8.6
  • FQBN de placa objetivo: arduino:avr:mega
  • Librerías Arduino (versiones exactas):
  • Ethernet@2.0.2 (controlador W5500 compatible)
  • SD@1.2.4 (microSD del shield)
  • ModbusMaster@2.0.1 (maestro Modbus RTU)
  • SPI (incluida en el core arduino:avr)
  • Compilador/avrdude: el core arduino:avr@1.8.6 incluye las herramientas necesarias (no instales avrdude por separado).

Conocimientos previos

  • Experiencia con C/C++ para Arduino.
  • Conocimientos prácticos de Modbus RTU (función 0x04: Input Registers).
  • Redes IP básicas (IPv4, máscara, gateway, DNS).
  • Manejo de bus RS‑485 (terminación, polarización, topología bus).

Materiales

  • 1x Arduino Mega 2560 (genuino o compatible).
  • 1x W5500 Ethernet Shield compatible con Arduino Mega (con microSD).
  • 1x Transceptor RS‑485 basado en MAX485 (módulo típico con pines RO/RE/DE/DI/A/B/VCC/GND).
  • 1x Medidor de energía con Modbus RTU (ejemplo: Eastron SDM120‑M o equivalente RS‑485, dirección esclavo 1).
  • 1x Tarjeta microSD formateada FAT32 (clase 10 recomendada, 4–32 GB).
  • 1x Fuente de alimentación estable para el contador de energía (según hoja de datos).
  • Cableado:
  • 1x Cable Ethernet Cat5e/6 para el W5500.
  • Cables dupont para interconexión con el MAX485.
  • Par trenzado para RS‑485 (A/B) con terminación 120 Ω en extremos.
  • Resistencias de terminación RS‑485:
  • 1x 120 Ω en el extremo del bus opuesto al MAX485 (si el contador no la incluye).
  • 1x 120 Ω cerca del MAX485 entre A y B (opcional, según topología).
  • PC de desarrollo con red Ethernet en el mismo segmento IP que el W5500.

Nota: El dispositivo objetivo exacto es Arduino Mega 2560 + W5500 Ethernet Shield + MAX485. Mantendremos coherencia con este conjunto en todo el caso práctico.

Preparación y conexión

Configuración RS‑485 (MAX485 ↔ Arduino Mega 2560)

  • Seleccionaremos Serial1 del Mega (TX1→18, RX1→19) para Modbus RTU.
  • Usaremos un único pin de control para DE/RE en modo half‑duplex.

Tabla de cableado MAX485:

Señal MAX485 Conectar a Arduino Mega 2560 Detalles
RO (Receiver Out) RX1 (pin 19) Datos desde el bus RS‑485 hacia el Mega
DI (Driver In) TX1 (pin 18) Datos desde el Mega hacia el bus RS‑485
DE (Driver Enable) D2 (pin digital 2) Habilita transmisión (HIGH)
RE (Receiver Enable) D2 (pin digital 2) Conectar junto con DE (LOW habilita recepción)
VCC 5V Alimentación del módulo MAX485
GND GND Masa común
A A del bus RS‑485 Línea diferencial A
B B del bus RS‑485 Línea diferencial B

Recomendaciones:
– Coloca una resistencia de 120 Ω entre A y B en el extremo más alejado si no está presente.
– Mantén el par A/B trenzado, con polarización si la instalación lo requiere.
– Evita ramificaciones (stubs) largas; usa topología bus.

Configuración del Shield W5500

  • Inserta el W5500 Ethernet Shield sobre el Mega 2560.
  • Conexiones SPI en Mega (hardware): MISO=50, MOSI=51, SCK=52, SS=53.
  • El W5500 usa típicamente CS en pin 10; la microSD usa CS en pin 4.
  • Asegúrate de:
  • pin 10 configurado como salida (evita triestado del SS).
  • SD y Ethernet no se seleccionen simultáneamente (el código gestiona CS apropiadamente).

Red y direccionamiento

  • Usaremos IP estática para el logger, por ejemplo 192.168.1.200/24.
  • Gateway y DNS típicos: 192.168.1.1.
  • Alternativamente, puedes usar DHCP; en este caso fijamos IP estática para previsibilidad del endpoint HTTP.

Registros Modbus del medidor de energía

Ejemplo basado en medidor tipo Eastron SDM120‑M (consulta tu hoja de datos y ajusta si difiere). Se leen como Input Registers (función 0x04), 2 registros por valor (float IEEE‑754, orden de palabra documentado por fabricante).

Magnitud Dirección base (dec) Registros Tipo Nota
Tensión (V) 0x0000 (0) 2 float32 Voltaje de línea
Corriente (A) 0x0006 (6) 2 float32 Corriente
Potencia activa (W) 0x000C (12) 2 float32 P activo instantáneo
Energía activa total (kWh) 0x0156 (342) 2 float32 Contador acumulado

Dirección de esclavo por defecto asumida: 1. Velocidad serial: 9600 8N1 (ajusta a 2400 8N1 si tu modelo lo requiere).

Código completo (Arduino C++)

Objetivos del firmware:
– Maestro Modbus RTU sobre RS‑485 (MAX485 + Serial1) para leer V, I, P, kWh.
– Servidor HTTP en W5500 para exponer métricas en texto y JSON (endpoints / y /metrics.json).
– Registro persistente en microSD (CSV, timestamps en UTC vía NTP).
– Sincronización NTP periódica (UDP).
– Opcional: publicación en InfluxDB (desactivada por defecto).

Bloques clave:
– Inicialización Ethernet y SD con pines de chip select correctos.
– Callbacks pre/post transmisión para DE/RE en MAX485.
– Conversión de dos registros Modbus a float (manejo de orden de palabras).
– Bucle de muestreo y servidor HTTP no bloqueante.

/*
  Modbus Energy Logger
  Dispositivo: Arduino Mega 2560 + W5500 Ethernet Shield + MAX485
  Toolchain: Arduino CLI 0.35.3, arduino:avr@1.8.6
  Librerías: Ethernet@2.0.2, SD@1.2.4, ModbusMaster@2.0.1
*/

#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include <SD.h>
#include <ModbusMaster.h>

// ======================== Configuración general =========================
#define FW_VERSION "1.0.0"
#define RS485_DE_RE_PIN 2
#define MODBUS_ID 1
#define MODBUS_BAUD 9600 // Ajusta a 2400 si tu medidor lo requiere
#define SERIAL_DEBUG_BAUD 115200
#define WORD_SWAP 1 // 1 si tu medidor usa intercambio de palabras (SDM suele requerirlo)

// Ethernet W5500
const uint8_t W5500_CS = 10;
const uint8_t SD_CS = 4;

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress ip(192, 168, 1, 200);
IPAddress dns(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
EthernetServer server(80);

// NTP
EthernetUDP udp;
const unsigned int NTP_LOCAL_PORT = 8888;
const char* NTP_HOST = "pool.ntp.org";
const unsigned long NTP_INTERVAL_MS = 3600000UL; // 1h
unsigned long lastNtpSync = 0;
unsigned long currentEpoch = 0; // Segundos desde 1970
unsigned long lastEpochUpdateMs = 0;

// SD
File logFile;
const char* LOG_FILENAME = "energy_log.csv";

// Estado de medición
struct Metrics {
  float voltage = NAN;
  float current = NAN;
  float power = NAN;
  float energy = NAN;
  uint8_t lastModbusStatus = 0xFF; // 0 = OK
  unsigned long lastSampleMs = 0;
  unsigned long okCount = 0;
  unsigned long errCount = 0;
} metrics;

const unsigned long SAMPLE_PERIOD_MS = 5000;

// ModbusMaster en Serial1
ModbusMaster node;

// ======================== Utilidades =========================
void preTransmission() {
  digitalWrite(RS485_DE_RE_PIN, HIGH); // Habilitar transmisión
}

void postTransmission() {
  digitalWrite(RS485_DE_RE_PIN, LOW); // Volver a recepción
}

float regsToFloat(uint16_t w0, uint16_t w1) {
  union {
    uint32_t u32;
    float f;
  } u;
#if WORD_SWAP
  // Muchos medidores entregan word swap (w1:w0)
  u.u32 = ((uint32_t)w1 << 16) | w0;
#else
  u.u32 = ((uint32_t)w0 << 16) | w1;
#endif
  return u.f;
}

bool readFloatInputRegister(uint16_t address, float &outVal, uint8_t &status) {
  // Solicita 2 registros (float32)
  uint8_t result = node.readInputRegisters(address, 2);
  status = result;
  if (result == node.ku8MBSuccess) {
    uint16_t w0 = node.getResponseBuffer(0);
    uint16_t w1 = node.getResponseBuffer(1);
    outVal = regsToFloat(w0, w1);
    node.clearResponseBuffer();
    return true;
  } else {
    node.clearResponseBuffer();
    return false;
  }
}

// Tiempo: actualiza currentEpoch usando millis cuando no hay NTP
void softTickEpoch() {
  unsigned long now = millis();
  if (lastEpochUpdateMs == 0) {
    lastEpochUpdateMs = now;
    return;
  }
  unsigned long delta = now - lastEpochUpdateMs;
  if (delta >= 1000) {
    currentEpoch += (delta / 1000);
    lastEpochUpdateMs += (delta / 1000) * 1000;
  }
}

void sendNTPPacket(IPAddress& address) {
  byte packetBuffer[48];
  memset(packetBuffer, 0, 48);
  packetBuffer[0] = 0b11100011; // LI, Version, Mode
  packetBuffer[1] = 0;          // Stratum
  packetBuffer[2] = 6;          // Polling Interval
  packetBuffer[3] = 0xEC;       // Precision
  // Transmit timestamp
  udp.beginPacket(address, 123);
  udp.write(packetBuffer, 48);
  udp.endPacket();
}

bool syncNTP() {
  IPAddress ntpIP;
  if (!Ethernet.hostByName(NTP_HOST, ntpIP)) {
    return false;
  }
  sendNTPPacket(ntpIP);
  unsigned long start = millis();
  while (millis() - start < 1500) {
    int size = udp.parsePacket();
    if (size >= 48) {
      byte packetBuffer[48];
      udp.read(packetBuffer, 48);
      unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
      unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
      unsigned long secsSince1900 = (highWord << 16) | lowWord;
      const unsigned long seventyYears = 2208988800UL;
      currentEpoch = secsSince1900 - seventyYears;
      lastEpochUpdateMs = millis();
      lastNtpSync = millis();
      return true;
    }
  }
  return false;
}

String timeToISO8601(unsigned long epoch) {
  // Conversión simple a YYYY-MM-DDTHH:MM:SSZ (UTC) sin librerías pesadas
  // Nota: cálculo aproximado; suficiente para logs. Para precisión total usa una librería RTC.
  unsigned long t = epoch;
  int sec = t % 60; t /= 60;
  int min = t % 60; t /= 60;
  int hour = t % 24;
  // Cálculo de fecha aproximado (no contempla bisiestos perfectos). Como mejora: implementar algoritmo civil completo.
  // Para un logger, el NTP da hora correcta y esta función sirve como referencia legible.
  // Implementación de fecha canónica simplificada:
  unsigned long days = epoch / 86400UL;
  // Epoch 1970-01-01 es jueves
  int year = 1970;
  const int daysInMonth[] = {31,28,31,30,31,30,31,31,30,31,30,31};
  while (true) {
    bool leap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
    unsigned long diy = 365 + (leap ? 1 : 0);
    if (days >= diy) { days -= diy; year++; }
    else break;
  }
  int month = 0;
  while (true) {
    bool leap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
    int dim = daysInMonth[month];
    if (leap && month == 1) dim = 29;
    if ((int)days >= dim) { days -= dim; month++; }
    else break;
  }
  int day = (int)days + 1;
  char buf[25];
  snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02dZ",
           year, month + 1, day, hour, min, sec);
  return String(buf);
}

void ensureLogHeader() {
  if (!SD.exists(LOG_FILENAME)) {
    logFile = SD.open(LOG_FILENAME, FILE_WRITE);
    if (logFile) {
      logFile.println(F("timestamp_utc,voltage_v,current_a,power_w,energy_kwh,modbus_status"));
      logFile.flush();
      logFile.close();
    }
  }
}

void appendLog() {
  logFile = SD.open(LOG_FILENAME, FILE_WRITE);
  if (logFile) {
    String ts = timeToISO8601(currentEpoch);
    logFile.print(ts); logFile.print(',');
    if (isnan(metrics.voltage)) logFile.print("NaN"); else logFile.print(metrics.voltage, 3);
    logFile.print(',');
    if (isnan(metrics.current)) logFile.print("NaN"); else logFile.print(metrics.current, 3);
    logFile.print(',');
    if (isnan(metrics.power)) logFile.print("NaN"); else logFile.print(metrics.power, 3);
    logFile.print(',');
    if (isnan(metrics.energy)) logFile.print("NaN"); else logFile.print(metrics.energy, 3);
    logFile.print(',');
    logFile.println(metrics.lastModbusStatus, DEC);
    logFile.flush();
    logFile.close();
  }
}

void printBanner() {
  Serial.println(F("Modbus Energy Logger"));
  Serial.print(F("FW: ")); Serial.println(FW_VERSION);
  Serial.println(F("Board: Arduino Mega 2560 + W5500 + MAX485"));
  Serial.println(F("Toolchain: Arduino CLI 0.35.3, core arduino:avr@1.8.6"));
  Serial.println(F("Libs: Ethernet@2.0.2, SD@1.2.4, ModbusMaster@2.0.1"));
}

// ======================== Setup =========================
void setup() {
  pinMode(RS485_DE_RE_PIN, OUTPUT);
  digitalWrite(RS485_DE_RE_PIN, LOW); // Recepción por defecto

  Serial.begin(SERIAL_DEBUG_BAUD);
  while (!Serial) { ; }

  printBanner();

  // Ethernet + SD
  Ethernet.init(W5500_CS);
  Serial.println(F("Inicializando Ethernet..."));
  Ethernet.begin(mac, ip, dns, gateway, subnet);
  delay(1000);
  if (Ethernet.hardwareStatus() == EthernetNoHardware) {
    Serial.println(F("ERROR: No se detecta hardware Ethernet (W5500)."));
  }
  if (Ethernet.linkStatus() == LinkOFF) {
    Serial.println(F("ADVERTENCIA: Sin enlace Ethernet (cable desconectado?)."));
  }
  Serial.print(F("IP: ")); Serial.println(Ethernet.localIP());
  server.begin();

  // UDP para NTP
  udp.begin(NTP_LOCAL_PORT);
  if (syncNTP()) {
    Serial.print(F("NTP OK: ")); Serial.println(timeToISO8601(currentEpoch));
  } else {
    Serial.println(F("NTP falló; se usará soft tick hasta próximo intento."));
  }

  // SD
  Serial.print(F("Inicializando SD (CS=4)... "));
  if (!SD.begin(SD_CS)) {
    Serial.println(F("ERROR"));
  } else {
    Serial.println(F("OK"));
    ensureLogHeader();
  }

  // Modbus RTU en Serial1
  Serial1.begin(MODBUS_BAUD, SERIAL_8N1);
  node.begin(MODBUS_ID, Serial1);
  node.preTransmission(preTransmission);
  node.postTransmission(postTransmission);
  Serial.println(F("Modbus RTU inicializado en Serial1."));
}

// ======================== Lógica de muestreo =========================
void sampleOnce() {
  uint8_t st = 0;
  bool okAll = true;

  float v = NAN, i = NAN, p = NAN, e = NAN;

  if (!readFloatInputRegister(0x0000, v, st)) okAll = false;
  if (!readFloatInputRegister(0x0006, i, st)) okAll = false;
  if (!readFloatInputRegister(0x000C, p, st)) okAll = false;
  if (!readFloatInputRegister(0x0156, e, st)) okAll = false;

  metrics.lastSampleMs = millis();
  metrics.lastModbusStatus = st;

  if (okAll) {
    metrics.voltage = v;
    metrics.current = i;
    metrics.power = p;
    metrics.energy = e;
    metrics.okCount++;
  } else {
    metrics.errCount++;
  }

  // Log a SD si está disponible
  if (SD.cardSize() > 0) {
    appendLog();
  }

  Serial.print(F("[SAMPLE] ts=")); Serial.print(timeToISO8601(currentEpoch));
  Serial.print(F(" V=")); Serial.print(metrics.voltage, 3);
  Serial.print(F(" I=")); Serial.print(metrics.current, 3);
  Serial.print(F(" P=")); Serial.print(metrics.power, 3);
  Serial.print(F(" E=")); Serial.print(metrics.energy, 3);
  Serial.print(F(" st=")); Serial.print(metrics.lastModbusStatus);
  Serial.print(F(" ok=")); Serial.print(metrics.okCount);
  Serial.print(F(" err=")); Serial.println(metrics.errCount);
}

// ======================== Servidor HTTP =========================
void handleClient(EthernetClient &client) {
  // Parseo muy simple de primera línea
  String req = client.readStringUntil('\n');
  req.trim();

  // Consume cabeceras restantes
  while (client.connected()) {
    String line = client.readStringUntil('\n');
    if (line == "\r" || line.length() == 0) break;
  }

  bool json = false;
  if (req.startsWith("GET /metrics.json")) json = true;

  if (req.startsWith("GET /") && !json) {
    client.println(F("HTTP/1.1 200 OK"));
    client.println(F("Content-Type: text/plain; charset=utf-8"));
    client.println(F("Connection: close"));
    client.println();
    client.print(F("fw=")); client.println(FW_VERSION);
    client.print(F("ip=")); client.println(Ethernet.localIP());
    client.print(F("time_utc=")); client.println(timeToISO8601(currentEpoch));
    client.print(F("voltage_v=")); client.println(isnan(metrics.voltage)?NAN:metrics.voltage, 3);
    client.print(F("current_a=")); client.println(isnan(metrics.current)?NAN:metrics.current, 3);
    client.print(F("power_w=")); client.println(isnan(metrics.power)?NAN:metrics.power, 3);
    client.print(F("energy_kwh=")); client.println(isnan(metrics.energy)?NAN:metrics.energy, 3);
    client.print(F("modbus_status=")); client.println(metrics.lastModbusStatus);
    client.print(F("ok_count=")); client.println(metrics.okCount);
    client.print(F("err_count=")); client.println(metrics.errCount);
    return;
  }

  if (json) {
    client.println(F("HTTP/1.1 200 OK"));
    client.println(F("Content-Type: application/json; charset=utf-8"));
    client.println(F("Cache-Control: no-cache"));
    client.println(F("Connection: close"));
    client.println();
    client.print(F("{\"fw\":\"")); client.print(FW_VERSION); client.print(F("\","));
    client.print(F("\"ip\":\"")); client.print(Ethernet.localIP()); client.print(F("\","));
    client.print(F("\"time_utc\":\"")); client.print(timeToISO8601(currentEpoch)); client.print(F("\","));
    client.print(F("\"voltage_v\":")); client.print(isnan(metrics.voltage)?0:metrics.voltage, 3); client.print(F(","));
    client.print(F("\"current_a\":")); client.print(isnan(metrics.current)?0:metrics.current, 3); client.print(F(","));
    client.print(F("\"power_w\":")); client.print(isnan(metrics.power)?0:metrics.power, 3); client.print(F(","));
    client.print(F("\"energy_kwh\":")); client.print(isnan(metrics.energy)?0:metrics.energy, 3); client.print(F(","));
    client.print(F("\"modbus_status\":")); client.print(metrics.lastModbusStatus); client.print(F(","));
    client.print(F("\"ok_count\":")); client.print(metrics.okCount); client.print(F(","));
    client.print(F("\"err_count\":")); client.print(metrics.errCount); client.print(F("}"));
    return;
  }

  // Si ruta no reconocida
  client.println(F("HTTP/1.1 404 Not Found"));
  client.println(F("Content-Type: text/plain"));
  client.println(F("Connection: close"));
  client.println();
  client.println(F("Not Found"));
}

// ======================== Loop =========================
void loop() {
  softTickEpoch();
  // NTP periódico
  if (millis() - lastNtpSync > NTP_INTERVAL_MS) {
    syncNTP();
  }

  // Muestreo periódico
  static unsigned long lastSample = 0;
  if (millis() - lastSample >= SAMPLE_PERIOD_MS) {
    lastSample = millis();
    sampleOnce();
  }

  // Webserver
  EthernetClient client = server.available();
  if (client) {
    handleClient(client);
    delay(1);
    client.stop();
  }
}

Notas sobre el código:
– RS485_DE_RE_PIN controla el transceptor MAX485: HIGH para transmitir, LOW para recibir. ModbusMaster usa callbacks para conmutarlo al enviar.
– WORD_SWAP ajusta el orden de palabras; si ves valores absurdos, prueba a cambiarlo a 0.
– Se usa NTP para registrar timestamps UTC legibles. Si NTP falla, el tiempo “avanza” por soft tick (menos preciso).
– Endpoints:
– GET / → texto plano rápido de leer.
– GET /metrics.json → JSON para integración con dashboards.
– CSV en SD: energy_log.csv con cabecera.

Compilación, carga y ejecución

Usaremos Arduino CLI 0.35.3 con el core arduino:avr@1.8.6 y FQBN arduino:avr:mega.

1) Instalar Arduino CLI

  • Windows (PowerShell):
  • Descarga desde https://arduino.github.io/arduino-cli/latest/installation/
  • Añade arduino-cli.exe al PATH.
  • macOS (Homebrew):
  • brew update
  • brew install arduino-cli
  • Ubuntu:
  • curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
  • Mueve el binario a /usr/local/bin si procede.

Verifica versión:

arduino-cli version

Salida esperada incluye: Version: 0.35.3

2) Configurar core y librerías (versiones exactas)

arduino-cli core update-index
arduino-cli core install arduino:avr@1.8.6
arduino-cli lib install "Ethernet@2.0.2" "SD@1.2.4" "ModbusMaster@2.0.1"

3) Estructura del sketch

Crea una carpeta para el proyecto:

mkdir -p ~/proyectos/modbus-energy-logger

Guarda el código anterior como:

~/proyectos/modbus-energy-logger/modbus-energy-logger.ino

4) Detectar el puerto serie

  • Linux:
  • Conecta el Mega 2560 y ejecuta:
    arduino-cli board list
    Deberías ver algo como /dev/ttyACM0 o /dev/ttyACM1.
  • macOS: /dev/cu.usbmodemXXXX
  • Windows: COM3, COM4, etc.

5) Compilar para Arduino Mega 2560

arduino-cli compile --fqbn arduino:avr:mega ~/proyectos/modbus-energy-logger

6) Cargar firmware

  • Linux/macOS:
    arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega ~/proyectos/modbus-energy-logger
  • Windows (ejemplo COM3):
    arduino-cli upload -p COM3 --fqbn arduino:avr:mega ~/proyectos/modbus-energy-logger

7) Monitor serie (para depuración)

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

Deberías ver el banner, IP, estado NTP y muestreos periódicos.

Validación paso a paso

1) Verificación física

  • Revisa que:
  • W5500 esté firmemente acoplado al Mega 2560.
  • Cable Ethernet conectado y LED de enlace encendido en el RJ45.
  • MAX485 cableado como en la tabla, con DE/RE a D2, TX1→DI (18), RX1→RO (19), VCC a 5V y GND común.
  • A/B hacia el medidor respetando polaridad y terminación (120 Ω en extremos).
  • Medidor energizado y configurado con dirección 1 y 9600 8N1 (o ajusta el sketch).

2) Comprobación de red

  • Desde tu PC en la misma red:
  • Ping:
    ping 192.168.1.200
    Respuestas estables indican conectividad IP correcta.
  • HTTP texto:
    curl http://192.168.1.200/
    Deberías ver claves como fw, ip, time_utc, voltage_v, etc.
  • HTTP JSON:
    curl http://192.168.1.200/metrics.json
    Deberías recibir un objeto JSON con las métricas.

3) Monitor serie

  • Abre el monitor:
    arduino-cli monitor -p /dev/ttyACM0 -c 115200
  • Observa líneas [SAMPLE] cada ~5 s con V, I, P, E y status (st=0 cuando es OK).
  • Verifica que okCount aumenta y errCount permanece en 0 (o bajo).

4) Validación Modbus

  • Si el medidor tiene display, compara:
  • Voltaje del display con voltage_v (tolerancia ±1%).
  • Potencia con power_w (tolerancia según modelo).
  • Energía acumulada con energy_kwh.
  • Si los valores son absurdos (e.g., 1.2e-38), cambia WORD_SWAP a 0 en el sketch, recompila y carga.

5) Registro en microSD

  • Deja correr el sistema unos minutos para generar datos.
  • Apaga la placa o desmonta la SD con el sistema inactivo para evitar corrupción.
  • Lee la SD en tu PC y abre energy_log.csv. Deberías ver cabecera y filas:
  • timestamp_utc en ISO8601.
  • Voltaje, corriente, potencia y energía con 3 decimales.
  • modbus_status=0 indica lectura correcta.

6) NTP

  • Tras el arranque, el log debe mostrar un NTP OK si hubo red y DNS.
  • Revisa time_utc y la marca temporal del CSV: deben ser coherentes en UTC.

Troubleshooting

1) Sin respuesta HTTP / ping
– Síntomas: no responde a ping; Ethernet.linkStatus() = LinkOFF.
– Causas:
– Cable Ethernet defectuoso o desconectado.
– IP en segmento distinto o conflicto de IP.
– Soluciones:
– Cambia cable/puerto; verifica LEDs del RJ45.
– Ajusta IP/gateway/subnet en el sketch acorde a tu red.
– Comprueba que tu PC está en el mismo segmento.

2) SD no inicializa (Inicializando SD… ERROR)
– Causas:
– CS incorrecto (no 4), tarjeta exFAT, mala inserción o tarjeta dañada.
– Soluciones:
– Asegura SD_CS=4 y que Ethernet no selecciona el bus simultáneamente.
– Reformatea a FAT32 con tamaño de asignación por defecto.
– Prueba otra microSD.

3) Lecturas Modbus devuelven NaN o valores imposibles
– Causas:
– Dirección de esclavo incorrecta, A/B invertidos, falta de terminación.
– Velocidad/paridad no coinciden con el medidor.
– Orden de palabras distinto.
– Soluciones:
– Verifica MODBUS_ID, conecta A↔A y B↔B; añade 120 Ω si necesario.
– Ajusta MODBUS_BAUD y formato 8N1 según el manual del medidor (si usa 2400 8N1, actualiza Serial1.begin).
– Cambia WORD_SWAP entre 1 y 0 y vuelve a probar.

4) Muchos errores st != 0 (errCount crece)
– Causas:
– Ruido en el bus, longitudes excesivas, falta de GND común.
– Soluciones:
– Usa par trenzado y evita stubs; asegure GND común entre MAX485 y medidor.
– Añade resistencias de polarización si el bus lo requiere (típico 680 Ω a 5V y GND en A/B).
– Reduce periodo de muestreo si saturas el medidor.

5) El W5500 bloquea la SD o viceversa
– Causas:
– Manejo incorrecto de CS en SPI compartido.
– Soluciones:
– Garantiza CS de W5500 (10) y SD (4) configurados como OUTPUT y seleccionados de a uno.
– Evita acceder a SD dentro de interrupciones.
– En este sketch SD y Ethernet se usan secuencialmente; respeta ese patrón.

6) Carga (upload) falla con avrdude/timeout
– Causas:
– Puerto serie equivocado; permisos en Linux; cable USB de carga sin datos.
– Soluciones:
– Verifica arduino-cli board list y usa el puerto correcto.
– En Linux: agrega tu usuario a dialout y reingresa (sudo usermod -a -G dialout $USER).
– Usa un cable USB “de datos” conocido.

7) NTP no sincroniza
– Causas:
– DNS/gateway incorrectos; firewall bloquea UDP/123.
– Soluciones:
– Verifica dns y gateway en el sketch.
– Prueba con IP de un servidor NTP local y ajusta hostByName si no tienes DNS.
– Permite UDP/123 en la red.

8) Respuesta HTTP incompleta o desconexiones
– Causas:
– Cliente que mantiene conexiones abiertas; recursos limitados.
– Soluciones:
– El servidor fuerza Connection: close; reintenta la petición.
– Reduce frecuencia de muestreo si la red es lenta.

Mejoras/variantes

  • Soporte multi‑esclavo: si tienes varios medidores en el mismo bus RS‑485, itera un vector de direcciones MODBUS_ID y multiplica el conjunto de métricas y rutas (e.g., /metrics.json?id=3).
  • Añadir ruta para descargar CSV actual (streaming de energy_log.csv) desde el servidor HTTP para no extraer la SD.
  • Integración con InfluxDB o Prometheus:
  • InfluxDB (v1) vía HTTP POST a /write?db=… con line protocol; encender opcional en el firmware.
  • Prometheus: exponer /metrics en formato Prometheus exposition text.
  • RTC hardware (DS3231) para timestamp robusto sin dependencia de red.
  • DHCP con fallback: intentar DHCP 10 s y, si falla, usar IP estática.
  • Seguridad de datos:
  • Buffer circular en RAM y escritura a SD por lotes para reducir desgaste.
  • Verificación de integridad del log (checksums de bloque).
  • Supervisión:
  • Señal de latido (LED) y contador de watchdog.
  • Exponer conteo de errores Modbus por función y timeout.

Checklist de verificación

  • [ ] He instalado Arduino CLI 0.35.3 y verificado con arduino-cli version.
  • [ ] He instalado el core arduino:avr@1.8.6 y las librerías Ethernet@2.0.2, SD@1.2.4, ModbusMaster@2.0.1.
  • [ ] El hardware corresponde a Arduino Mega 2560 + W5500 Ethernet Shield + MAX485.
  • [ ] El MAX485 está cableado: TX1→DI (18), RX1→RO (19), DE/RE→D2, VCC→5V, GND común, A/B correctos y con terminación cuando aplica.
  • [ ] El shield W5500 tiene Ethernet CS=10 y SD CS=4; el cable Ethernet está conectado y hay enlace.
  • [ ] El medidor Modbus RTU está energizado, con dirección 1 y velocidad 9600 8N1 (o he ajustado el sketch).
  • [ ] He compilado con: arduino-cli compile –fqbn arduino:avr:mega …
  • [ ] He cargado con: arduino-cli upload -p –fqbn arduino:avr:mega …
  • [ ] Puedo hacer ping a 192.168.1.200 y acceder a http://192.168.1.200/ y /metrics.json.
  • [ ] Veo muestras en el monitor serie con st=0 y okCount creciente.
  • [ ] El archivo energy_log.csv en la SD contiene cabecera y registros con timestamps en UTC.
  • [ ] He validado que los valores reportados son coherentes con el display del medidor (dentro de tolerancia).
  • [ ] Si vi valores incoherentes, ajusté WORD_SWAP y/o la configuración serial y lo comprobé de nuevo.

Con este caso práctico tendrás un “modbus-energy-logger” robusto y reproducible basado en Arduino Mega 2560 + W5500 Ethernet Shield + MAX485, con toolchain exacto, comandos concretos y validaciones claras para asegurar el correcto funcionamiento.

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

Ir a Amazon

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

Quiz rápido

Pregunta 1: ¿Cuál es la versión de Arduino CLI utilizada en la validación?




Pregunta 2: ¿Qué tipo de placa es la placa objetivo mencionada?




Pregunta 3: ¿Cuál es la versión de la librería Ethernet utilizada?




Pregunta 4: ¿Qué compuerta lógica se utiliza para el bus RS-485?




Pregunta 5: ¿Qué protocolo se utiliza para la comunicación del medidor de energía?




Pregunta 6: ¿Cuál es la versión del core de placas AVR utilizada?




Pregunta 7: ¿Qué tipo de tarjeta microSD se recomienda?




Pregunta 8: ¿Qué herramienta incluye el core arduino:avr@1.8.6?




Pregunta 9: ¿Qué tipo de cable se recomienda para la interconexión con el MAX485?




Pregunta 10: ¿Qué función de Modbus RTU se menciona en el contexto?




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

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

Sígueme:


Caso práctico: Acceso NFC por WiFi con Arduino Nano 33 IoT

Caso práctico: Acceso NFC por WiFi con Arduino Nano 33 IoT — hero

Objetivo y caso de uso

Qué construirás: Un sistema de control de acceso NFC a través de WiFi utilizando un Arduino Nano 33 IoT y un módulo PN532.

Para qué sirve

  • Control de acceso a instalaciones mediante tarjetas NFC.
  • Monitoreo remoto de entradas y salidas a través de una aplicación web.
  • Integración con sistemas de seguridad existentes mediante MQTT.
  • Visualización de datos de acceso en tiempo real en una pantalla OLED SSD1306.

Resultado esperado

  • Tiempo de respuesta de acceso inferior a 500 ms.
  • Capacidad de manejar hasta 100 accesos por hora sin pérdida de datos.
  • Latencia de comunicación WiFi menor a 100 ms.
  • Registro de accesos en tiempo real con actualizaciones cada 5 segundos.

Público objetivo: Desarrolladores y entusiastas de IoT; Nivel: Avanzado

Arquitectura/flujo: Arduino Nano 33 IoT <-> Módulo NFC PN532 <-> WiFi <-> Servidor MQTT <-> Aplicación web

Nivel: Avanzado

Prerrequisitos

Sistema operativo y entorno probado

  • Windows 11 Pro 23H2 (build 22631.x)
  • Ubuntu 22.04.4 LTS (kernel 5.15.x)
  • macOS 13 Ventura

Nota: El proyecto es multiplataforma. Los comandos de compilación/flash se dan con PlatformIO CLI.

Toolchain exacta (versiones)

  • Python 3.11.6 (requerido por PlatformIO)
  • PlatformIO Core 6.1.14 (CLI)
  • Plataforma PlatformIO: atmelsam@8.3.0
  • Placa PlatformIO: nano_33_iot (Arduino Nano 33 IoT)
  • Framework: Arduino (proporcionado por atmelsam)
  • Librerías Arduino (versiones mínimas/pinneadas en PlatformIO):
  • Adafruit PN532@1.2.1
  • Adafruit SSD1306@2.5.9
  • Adafruit GFX Library@1.11.10
  • WiFiNINA@1.8.13

Notas de drivers:
– Arduino Nano 33 IoT usa USB nativo (CDC ACM). En Windows 10/11, el driver es estándar y no se requieren CP210x/CH34x. En Linux, añadir reglas udev para acceso sin sudo (ver Troubleshooting).
– No se requiere Arduino IDE; se usa exclusivamente PlatformIO CLI.

Materiales

  • 1x Arduino Nano 33 IoT (modelo exacto: Arduino Nano 33 IoT, MCU SAMD21 + módulo NINA-W102, 3.3 V lógico).
  • 1x Módulo NFC PN532 con soporte I2C y pines IRQ/RESET accesibles. Asegúrate de poder configurarlo en modo I2C.
  • 1x Pantalla OLED SSD1306 0.96” (128×64, interfaz I2C, dirección 0x3C habitual).
  • Tarjetas/llaveros NFC tipo MIFARE Classic/Ultralight (ISO14443A).
  • Protoboard y/o cables Dupont macho‑hembra.
  • Cable USB micro/USB‑C según tu cable para el Nano 33 IoT.
  • Fuente USB 5 V (alimentación por el puerto del Nano).
  • Opcional: Resistencias pull‑up I2C si tus módulos no las integran (4.7 kΩ a 3.3 V en SDA/SCL). La mayoría de SSD1306/PN532 ya las incluyen.

Importante:
– El Arduino Nano 33 IoT es 3.3 V. No conectes módulos de 5 V a líneas lógicas sin nivelación.

Preparación y conexión

Configuración del PN532

  • Coloca el PN532 en modo I2C:
  • En módulos con microinterruptores SEL0/SEL1: I2C suele ser SEL0=ON, SEL1=OFF. Verifica en la serigrafía/hoja de datos de TU módulo.
  • En placas con “jumpers” de soldadura (I2C/SPI/UART): une el pad de I2C según indique el fabricante.
  • Asegúrate de que el pin IRQ y el pin RST (o RSTO) estén accesibles.

Cableado propuesto (I2C compartido para PN532 y OLED)

  • Bus I2C común (SDA/SCL) a 3.3 V.
  • PN532 por I2C usando IRQ/RESET con pines digitales del Nano para notificación y reinicio.
  • SSD1306 por I2C (dirección por defecto 0x3C).

Tabla de conexiones

Elemento Pin en Arduino Nano 33 IoT Pin en PN532 Pin en OLED SSD1306
Alimentación 3V3 VCC (3V3) VCC (3V3)
GND GND GND GND
I2C SDA SDA (marcado “SDA”/A4) SDA SDA
I2C SCL SCL (marcado “SCL”/A5) SCL SCL
IRQ PN532 D2 IRQ
RESET PN532 D3 RST o RSTO

Observaciones:
– La dirección I2C típica del SSD1306 es 0x3C. La del PN532 (7 bits) suele ser 0x24 cuando está en modo I2C (algunos datasheets la muestran como 0x48 en 8 bits). No hay conflicto habitual.
– Mantén los cables I2C lo más cortos posibles; si el bus es largo, reduce la frecuencia de I2C (por defecto 100 kHz) o mejora el apantallamiento.

Código completo (Arduino framework con PlatformIO)

El siguiente código implementa:
– Lectura de tarjetas/llaveros NFC con PN532 (ISO14443A).
– Lista blanca de UIDs autorizados en memoria de programa.
– Conexión a Wi‑Fi con WiFiNINA y envío de un POST HTTP a un endpoint (por defecto http://httpbin.org/post) registrando “concedido/denegado”.
– Visualización de estado en OLED SSD1306 (con Adafruit GFX).

Personalización:
– Las credenciales Wi‑Fi y el endpoint se pasan por macros de compilación definidas en platformio.ini (ver sección de compilación).

Archivo: src/main.cpp

#include <Arduino.h>
#include <Wire.h>
#include <SPI.h> // no se usa, pero muchas libs lo incluyen
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_PN532.h>
#include <WiFiNINA.h>
#include <WiFiClient.h>
#include <WiFiSSLClient.h>

// Configuración OLED
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); // reset -1 (compartido)

// Pines PN532 (I2C con IRQ y RST)
#define PN532_IRQ   2
#define PN532_RESET 3
Adafruit_PN532 nfc(PN532_IRQ, PN532_RESET);

// Parámetros Wi‑Fi y API (definidos en platformio.ini vía -D)
#ifndef WIFI_SSID
  #define WIFI_SSID "SSID_NO_DEF"
#endif
#ifndef WIFI_PASS
  #define WIFI_PASS "PASS_NO_DEF"
#endif
#ifndef API_HOST
  #define API_HOST "httpbin.org"
#endif
#ifndef API_PORT
  #define API_PORT 80
#endif
#ifndef API_PATH
  #define API_PATH "/post"
#endif

// Estructura de UID autorizados
struct Uid {
  uint8_t len;
  uint8_t bytes[7];
};

// Lista blanca de ejemplo (reemplaza con UIDs de tus tarjetas)
const Uid AUTH_UIDS[] PROGMEM = {
  {7, {0x04, 0xA2, 0xB1, 0xC2, 0xD3, 0xE4, 0xF5}}, // 7 bytes
  {4, {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00}}, // 4 bytes (resto relleno)
};
const size_t AUTH_UIDS_COUNT = sizeof(AUTH_UIDS) / sizeof(AUTH_UIDS[0]);

// Buffers NFC
uint8_t uid[7] = {0};
uint8_t uidLength = 0;

// Utilidad: convierte UID a hex string
String uidToHex(const uint8_t* u, uint8_t len) {
  const char hexmap[] = "0123456789ABCDEF";
  String s;
  s.reserve(len*2);
  for (uint8_t i = 0; i < len; i++) {
    s += hexmap[(u[i] >> 4) & 0x0F];
    s += hexmap[u[i] & 0x0F];
  }
  return s;
}

// Compara UID leído con lista blanca
bool isAuthorized(const uint8_t* u, uint8_t len) {
  for (size_t i = 0; i < AUTH_UIDS_COUNT; i++) {
    Uid entry;
    memcpy_P(&entry, &AUTH_UIDS[i], sizeof(Uid));
    if (entry.len != len) continue;
    if (memcmp(entry.bytes, u, len) == 0) return true;
  }
  return false;
}

// UI: escribir dos líneas centradas
void drawCentered(const String& l1, const String& l2) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  int16_t x1, y1;
  uint16_t w, h;

  display.getTextBounds(l1, 0, 0, &x1, &y1, &w, &h);
  int16_t x = (SCREEN_WIDTH - w) / 2;
  display.setCursor(x < 0 ? 0 : x, 10);
  display.println(l1);

  display.getTextBounds(l2, 0, 0, &x1, &y1, &w, &h);
  x = (SCREEN_WIDTH - w) / 2;
  display.setCursor(x < 0 ? 0 : x, 30);
  display.println(l2);

  display.display();
}

// Conexión Wi‑Fi con reintentos y feedback
bool wifiConnect(uint32_t timeoutMs = 20000) {
  uint32_t start = millis();
  if (WiFi.status() == WL_CONNECTED) return true;

  Serial.print(F("[WiFi] Conectando a SSID: "));
  Serial.println(F(WIFI_SSID));
  drawCentered("Wi-Fi", "Conectando...");

  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    if (millis() - start > timeoutMs) {
      Serial.println(F("[WiFi] Timeout"));
      drawCentered("Wi-Fi", "Timeout");
      return false;
    }
    delay(300);
    Serial.print('.');
  }
  Serial.println();
  Serial.print(F("[WiFi] Conectado. IP: "));
  Serial.println(WiFi.localIP());

  char buff[24];
  snprintf(buff, sizeof(buff), "IP %d.%d.%d.%d",
           WiFi.localIP()[0], WiFi.localIP()[1],
           WiFi.localIP()[2], WiFi.localIP()[3]);
  drawCentered("Wi-Fi OK", String(buff));
  delay(800);
  return true;
}

// Envío de registro por HTTP/HTTPS según API_PORT
bool postAccessEvent(const String& uidHex, bool granted) {
  // Selección de cliente: TLS si puerto 443, HTTP en otro caso
#if API_PORT == 443
  WiFiSSLClient client;
#else
  WiFiClient client;
#endif

  Serial.print(F("[HTTP] Conectando a "));
  Serial.print(F(API_HOST));
  Serial.print(':');
  Serial.println(API_PORT);

  if (!client.connect(API_HOST, API_PORT)) {
    Serial.println(F("[HTTP] Conexion fallida"));
    drawCentered("POST", "Conexion fallida");
    return false;
  }

  // Construir JSON y cabeceras
  String body = String("{\"device\":\"nano33iot\",") +
                "\"uid\":\"" + uidHex + "\"," +
                "\"result\":\"" + String(granted ? "granted" : "denied") + "\"}";

  String req = String("POST ") + API_PATH + " HTTP/1.1\r\n" +
               "Host: " + String(API_HOST) + "\r\n" +
               "User-Agent: nano33iot-nfc/1.0\r\n" +
               "Content-Type: application/json\r\n" +
               "Connection: close\r\n" +
               "Content-Length: " + String(body.length()) + "\r\n\r\n" +
               body;

  client.print(req);

  // Leer respuesta mínima (código de estado)
  uint32_t t0 = millis();
  while (!client.available()) {
    if (millis() - t0 > 7000) {
      Serial.println(F("[HTTP] Timeout esperando respuesta"));
      drawCentered("POST", "Timeout respuesta");
      client.stop();
      return false;
    }
    delay(50);
  }

  // Buscar línea de estado
  String statusLine = client.readStringUntil('\n'); // e.g., "HTTP/1.1 200 OK"
  Serial.print(F("[HTTP] Status: "));
  Serial.println(statusLine);
  bool ok = statusLine.indexOf("200") >= 0 || statusLine.indexOf("204") >= 0;

  // Consume y escribe un resumen
  int received = 0;
  while (client.available()) {
    client.read();
    received++;
  }
  Serial.print(F("[HTTP] Bytes en cuerpo: ~"));
  Serial.println(received);
  client.stop();

  drawCentered("POST", ok ? "OK" : "ERROR");
  delay(500);
  return ok;
}

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 3000) { /* espera USB */ }

  // OLED
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    // Sin pantalla, seguir pero informar por Serial
    Serial.println(F("[OLED] No inicializada (0x3C)"));
  } else {
    display.clearDisplay();
    display.display();
    drawCentered("NFC Wi-Fi", "Access Control");
  }

  // PN532
  Wire.begin(); // I2C
  nfc.begin();
  delay(50);

  uint32_t versiondata = nfc.getFirmwareVersion();
  if (!versiondata) {
    Serial.println(F("[PN532] No se detecta. Revisa cables/modo I2C."));
    drawCentered("PN532", "No detectado");
    while (true) { delay(1000); }
  }
  Serial.print(F("[PN532] Chip ver. 0x"));
  Serial.println(versiondata, HEX);
  nfc.SAMConfig(); // Modo normal, IRQ

  drawCentered("PN532", "Listo");

  // Wi‑Fi (opcional: solo conectar al primer acceso para ahorrar energía)
  int fwMajor = WiFi.firmwareVersion()[1] - '0';
  Serial.print(F("[WiFiNINA] FW: "));
  Serial.println(WiFi.firmwareVersion());
  // Sugerencia: FW >= 1.4.8 recomendado en Nano 33 IoT

  wifiConnect(20000);
}

void loop() {
  // Espera tarjeta
  drawCentered("Aproxime", "tarjeta NFC");
  bool success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 1000);
  if (!success) {
    // Nada detectado, loop
    return;
  }

  String uidHex = uidToHex(uid, uidLength);
  Serial.print(F("[NFC] UID: "));
  Serial.println(uidHex);
  drawCentered("NFC detectado", uidHex);

  bool granted = isAuthorized(uid, uidLength);
  if (granted) {
    Serial.println(F("[ACCESS] Autorizado"));
    drawCentered("ACCESO", "CONCEDIDO");
  } else {
    Serial.println(F("[ACCESS] Denegado"));
    drawCentered("ACCESO", "DENEGADO");
  }

  // Enviar log a servidor
  if (WiFi.status() != WL_CONNECTED) {
    wifiConnect(10000);
  }
  postAccessEvent(uidHex, granted);

  // Antirrebote: esperar a que se retire la tarjeta
  uint32_t tstart = millis();
  while (millis() - tstart < 1500) {
    uint8_t tmpLen = 0;
    uint8_t tmp[7];
    // Si todavía presente, reinicia contador
    if (nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, tmp, &tmpLen, 50)) {
      tstart = millis();
    }
    delay(50);
  }
}

Explicación breve de partes clave:
– Inicialización de OLED y PN532: se valida la presencia del PN532 (getFirmwareVersion) y se configura en modo SAM (SAMConfig) para lectura normal con IRQ.
– Lista blanca de UIDs: estructura Uid con longitud y bytes, permitiendo UIDs de 4 o 7 bytes. La comprobación es exacta.
– Wi‑Fi: conexión con reintento y feedback en OLED. Se recomienda firmware NINA actualizado (verificado con WiFi.firmwareVersion()).
– HTTP POST: cliente HTTP o HTTPS según el puerto definido. Se envía JSON con device, uid y result.
– Antirrebote de tarjeta: se espera a que la tarjeta se retire para evitar múltiples lecturas consecutivas.

Compilación, flash y ejecución

Asegúrate de tener Python 3.11 y pipx (o pip) instalados. Ejemplos:

  • Windows (PowerShell):
  • winget install Python.Python.3.11
  • pipx install platformio==6.1.14
  • Ubuntu:
  • sudo apt update && sudo apt install -y python3.11 python3.11-venv python3-pip
  • pipx install platformio==6.1.14
  • macOS:
  • brew install python@3.11
  • pip3.11 install –user pipx
  • python3.11 -m pipx ensurepath
  • pipx install platformio==6.1.14

Comprueba la versión:

pio --version
# PlatformIO Core, version 6.1.14

Inicializar proyecto y dependencias

1) Crea carpeta y proyecto:

mkdir nfc-wifi-access-control && cd nfc-wifi-access-control
pio project init --board nano_33_iot

2) Crea/edita el archivo platformio.ini (raíz del proyecto) con el contenido siguiente:

[env:nano_33_iot]
platform = atmelsam@8.3.0
board = nano_33_iot
framework = arduino
monitor_speed = 115200

; Dependencias exactas
lib_deps =
  adafruit/Adafruit PN532@^1.2.1
  adafruit/Adafruit SSD1306@^2.5.9
  adafruit/Adafruit GFX Library@^1.11.10
  arduino-libraries/WiFiNINA@^1.8.13

; Configuración de credenciales y API por macros de compilación
build_flags =
  -D WIFI_SSID=\"TuSSID\"
  -D WIFI_PASS=\"TuPassword\"
  ; Puedes usar httpbin.org (HTTP) para probar; para TLS usa puerto 443
  -D API_HOST=\"httpbin.org\"
  -D API_PORT=80
  -D API_PATH=\"/post\"

3) Crea el archivo src/main.cpp con el código del apartado anterior.

4) Compila:

pio run

Subida (flash) al Arduino Nano 33 IoT

1) Identifica el puerto serie:
– Windows: en el Administrador de dispositivos, “Puertos (COM y LPT)”, suele aparecer como COMx (Arduino Nano 33 IoT).
– Linux/macOS:

pio device list
# o
ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null

2) Sube el firmware (ajusta el puerto si fuese necesario):

pio run --target upload --upload-port COM5
# Linux/macOS ejemplo:
pio run --target upload --upload-port /dev/ttyACM0

3) Abre el monitor serie a 115200 baudios:

pio device monitor -b 115200

Salida esperada inicial (ejemplo):

[OLED] (si presente no hay error)
[PN532] Chip ver. 0x32FF01
[WiFiNINA] FW: 1.4.8
[WiFi] Conectando a SSID: TuSSID
...
[WiFi] Conectado. IP: 192.168.1.50

Validación paso a paso

1) Validar conexión física:
– Al energizar, la OLED debe encender (pantalla negra con backlight en algunos módulos) y mostrar “NFC Wi‑Fi / Access Control”.
– Si la OLED no muestra nada pero el Serial avanza sin errores, al menos el controlador SSD1306 respondió.

2) Validar PN532:
– En el monitor serie debes ver una línea tipo “[PN532] Chip ver. 0x3xxxx”. Si no aparece, revisar cableado, modo I2C y pines IRQ/RESET.
– La OLED debe mostrar “PN532 / Listo”.

3) Validar Wi‑Fi:
– Debe verse “[WiFi] Conectado. IP: …” y la OLED mostrar “Wi‑Fi OK / IP x.x.x.x”.
– Si falla, revisa SSID/Pass en platformio.ini y la cobertura.

4) Probar lectura NFC:
– Acerca una tarjeta MIFARE o llavero ISO14443A al PN532 (2–3 cm). En Serial verás:
– “[NFC] UID: 04A2B1C2D3E4F5” (ejemplo).
– “[ACCESS] Autorizado” o “[ACCESS] Denegado” según tu lista blanca.
– La OLED debe mostrar “NFC detectado” y el UID en la línea 2, seguido de “ACCESO CONCEDIDO” o “ACCESO DENEGADO”.

5) Validar POST HTTP:
– Tras el evento, verás:
– “[HTTP] Conectando a httpbin.org:80”
– “[HTTP] Status: HTTP/1.1 200 OK”
– “[HTTP] Bytes en cuerpo: ~xxx”
– Si usas httpbin.org/post, la respuesta es 200 OK. Alternativamente, puedes configurar:
– API_HOST=»httpbin.org», API_PORT=443, API_PATH=»/post» para HTTPS. Asegúrate de que tu WiFiNINA FW soporte TLS adecuadamente.

6) Verificación “end‑to‑end”:
– Modifica la lista blanca con tu UID real:
– Copia el UID impreso en Serial y reemplaza uno de los registros en AUTH_UIDS (respetando longitud).
– Compila y sube de nuevo. Repite la lectura y verifica que ahora sea “[ACCESS] Autorizado” y que el POST reporte result=granted.

7) Validación alternativa de servidor:
– Puedes apuntar a un endpoint propio (ej. Flask local) o a http://postman-echo.com/post:
– API_HOST=»postman-echo.com», API_PORT=80, API_PATH=»/post»
– En tu PC, verifica con tcpdump/Wireshark que el Nano 33 IoT realiza la conexión al host/puerto configurado.

Troubleshooting (errores típicos y soluciones)

1) PN532 no detectado (getFirmwareVersion devuelve 0):
– Causas:
– Módulo PN532 no está en modo I2C.
– IRQ/RESET no conectados o pines cambiados en el código.
– SDA/SCL invertidos o sin alimentación a 3.3 V.
– Soluciones:
– Revisa los jumpers/interruptores del PN532 para I2C.
– Verifica tabla de conexiones; comprueba continuidad con multímetro.
– Asegúrate de usar 3.3 V del Nano 33 IoT (NUNCA 5 V en señales).

2) Bloqueo al iniciar nfc.SAMConfig():
– Causas: bus I2C colgado por pull‑ups inexistentes o excesivamente débiles.
– Soluciones:
– Usa módulos con resistencias pull‑up integradas (habitual). Si no, añade 4.7 kΩ a 3.3 V en SDA y SCL.
– Reduce frecuencia I2C (Wire.setClock(100000)) antes de nfc.begin() si el cableado es largo.

3) OLED no inicializa (pantalla en negro, mensaje “[OLED] No inicializada”):
– Causas: dirección no es 0x3C, cableado incorrecto, alimentación insuficiente.
– Soluciones:
– Prueba con 0x3D en display.begin. Verifica la serigrafía/puente de dirección del OLED.
– Confirma SDA/SCL correctos y comunes con el PN532.
– Asegura GND común y alimentación a 3.3 V.

4) Error de conexión Wi‑Fi (timeout):
– Causas: SSID/clave incorrectos, filtrado MAC, señal débil.
– Soluciones:
– Corrige WIFI_SSID/WIFI_PASS en platformio.ini (sin caracteres especiales mal escapados).
– Acerca el router o usa banda 2.4 GHz (NINA‑W102 es 2.4 GHz).
– Reinicia el router para renovar DHCP si es necesario.

5) TLS falla en puerto 443 (HTTPS):
– Causas: firmware NINA obsoleto o cadena de certificados no soportada.
– Soluciones:
– Usa HTTP (puerto 80) para pruebas iniciales.
– Actualiza firmware NINA (>= 1.4.8 recomendado) con herramientas de Arduino (solo para actualizar FW).
– Alternativamente, usa un endpoint con TLS simple o desactiva SNI en pruebas (no recomendado para producción).

6) No se puede subir el firmware (upload) en Linux sin sudo:
– Causa: permisos de udev.
– Solución:
– Crea regla udev: /etc/udev/rules.d/99-arduino.rules con contenido como:
– SUBSYSTEM==»tty», ATTRS{idVendor}==»2341″, MODE=»0666″
– SUBSYSTEM==»tty», ATTRS{idVendor}==»2a03″, MODE=»0666″
– Luego: sudo udevadm control –reload-rules && sudo udevadm trigger

7) Port COM/ACM no aparece en Windows:
– Causas: cable USB solo carga, puerto USB defectuoso, driver bloqueado.
– Soluciones:
– Usa cable USB de datos comprobado.
– Cambia de puerto. Reinstala el dispositivo en el Administrador si aparece con advertencia.
– Pulsa dos veces el botón de reset del Nano para forzar el bootloader (el puerto puede cambiar temporalmente).

8) Lecturas múltiples indeseadas de la misma tarjeta:
– Causas: la tarjeta permanece en el campo y se disparan eventos continuos.
– Soluciones:
– El antirrebote en el loop ya espera a que se retire; ajusta la duración (1500 ms).
– Implementa lógica de “último UID y último tiempo” para ignorar repeticiones en ventana.

Mejoras y variantes

  • Seguridad del canal:
  • Usar HTTPS (API_PORT=443) con WiFiSSLClient, validando el certificado raíz (limitado por memoria) o usando fingerprint (huella SHA1/256) si tu endpoint lo permite.
  • Autenticación del mensaje:
  • Añadir HMAC (SHA‑256) del cuerpo con una clave precompartida y cabecera X‑HMAC. En el servidor, verificar el HMAC. Librerías sugeridas: ArduinoBearSSL o Crypto (ajustar a SAMD21).
  • Sincronización de hora:
  • Obtener hora por NTP y adjuntar timestamp firmado en la solicitud al servidor contra ataques de repetición.
  • OTA/Provisioning:
  • Implementar actualización OTA vía WiFiNINA (requiere servidor) y parametrización de credenciales mediante portal cautivo BLE/Wi‑Fi.
  • Gestión de UIDs:
  • Modo “aprendizaje”: mantener pulsado un botón al presentar una tarjeta para añadirla a EEPROM/Flash (persistencia), evitando recompilación.
  • Accionamiento físico:
  • Controlar un relé o actuador (a 3.3 V lógico, con transistor/MOSFET y diodo flyback) para abrir una cerradura al “ACCESO CONCEDIDO”.
  • MQTT:
  • Publicar eventos de acceso en un broker MQTT (tema “access/logs”) y recibir políticas/whitelist dinámicamente.
  • Métricas y diagnósticos:
  • Mostrar RSSI, tiempo de respuesta HTTP, conteo de eventos, y último código de estado en la OLED con páginas navegables.

Checklist de verificación

  • [ ] He instalado PlatformIO Core 6.1.14 y puedo ejecutar “pio –version”.
  • [ ] He creado el proyecto con “pio project init –board nano_33_iot”.
  • [ ] He copiado platformio.ini con las librerías y macros (-D WIFI_SSID/PASS, API_HOST/PORT/PATH).
  • [ ] He cableado el PN532 en modo I2C y conectado IRQ a D2 y RST a D3 del Nano 33 IoT.
  • [ ] He cableado el OLED SSD1306 a 3.3 V, GND, SDA y SCL (dirección 0x3C).
  • [ ] La compilación (“pio run”) termina sin errores.
  • [ ] La subida (“pio run –target upload –upload-port …”) funciona y el monitor serie abre a 115200.
  • [ ] El PN532 se detecta y la OLED muestra “PN532 Listo”.
  • [ ] El Nano 33 IoT se conecta a la Wi‑Fi y muestra la IP en la OLED.
  • [ ] Al acercar una tarjeta, veo el UID en Serial/OLED y el estado de “ACCESO CONCEDIDO/DENEGADO” según mi lista blanca.
  • [ ] El POST al endpoint devuelve 200 OK (httpbin/post u otro), confirmado en el monitor serie.

Apéndice: comandos útiles de PlatformIO

  • Listar puertos y dispositivos:
pio device list
  • Limpiar compilación:
pio run -t clean
  • Monitoreo serie con reconexión:
pio device monitor -b 115200 --echo
  • Forzar reinstalación de dependencias (si hay conflictos de versiones):
pio pkg update
pio pkg install

Con este caso práctico, has desplegado un control de acceso “nfc-wifi-access-control” sobre un Arduino Nano 33 IoT + PN532 NFC + SSD1306 OLED, con un flujo completo desde la lectura del UID hasta el registro de eventos en un servidor HTTP, manteniendo coherencia en hardware, conexión, código y comandos de toolchain.

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 recomendado para el entorno de prueba?




Pregunta 2: ¿Qué versión de Python es requerida por PlatformIO?




Pregunta 3: ¿Qué herramienta se utiliza exclusivamente para la compilación y el flasheo?




Pregunta 4: ¿Cuál es la placa específica mencionada para usar con PlatformIO?




Pregunta 5: ¿Qué tipo de módulo NFC se requiere en el proyecto?




Pregunta 6: ¿Qué dirección I2C es habitual para la pantalla OLED SSD1306?




Pregunta 7: ¿Qué tipo de tarjetas se utilizan en el proyecto?




Pregunta 8: ¿Qué voltaje lógico maneja el Arduino Nano 33 IoT?




Pregunta 9: ¿Qué se debe hacer si los módulos no tienen resistencias pull-up I2C integradas?




Pregunta 10: ¿Qué tipo de conexión USB se requiere para el Arduino Nano 33 IoT?




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

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

Sígueme:


Caso práctico: nodo LoRaWAN agro MKR WAN 1310+BME680+DS18B20

Caso práctico: nodo LoRaWAN agro MKR WAN 1310+BME680+DS18B20 — hero

Objetivo y caso de uso

Qué construirás: Un nodo de microclima LoRaWAN utilizando Arduino MKR WAN 1310, BME680 y DS18B20 para medir y transmitir datos ambientales en agricultura.

Para qué sirve

  • Monitoreo de temperatura y humedad del suelo mediante el sensor BME680.
  • Medición de la temperatura del aire utilizando el sensor DS18B20.
  • Transmisión de datos en tiempo real a través de LoRaWAN para análisis remoto.
  • Optimización del riego basado en datos ambientales.
  • Integración con plataformas de gestión agrícola para visualización de datos.

Resultado esperado

  • Transmisión de datos cada 15 minutos con una latencia menor a 5 segundos.
  • Precisión de medición de temperatura de ±0.5 °C y humedad de ±3%.
  • Consumo de energía del nodo menor a 100 mA durante la transmisión.
  • Capacidad de enviar hasta 10.000 paquetes de datos por mes.
  • Alertas automáticas si los parámetros ambientales superan umbrales críticos.

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

Arquitectura/flujo: Sensor BME680 y DS18B20 -> Arduino MKR WAN 1310 -> Transmisión LoRaWAN -> Plataforma de gestión.

Nivel: Avanzado

Prerrequisitos

  • Sistemas operativos probados:
  • Ubuntu 22.04 LTS (amd64)
  • Windows 11 23H2 (64-bit)
  • macOS 14 Sonoma (Apple Silicon o Intel)

  • Toolchain exacta (línea de comandos, sin IDE gráfico):

  • Arduino CLI 1.1.1
  • Core “Arduino SAMD Boards (32-bits ARM Cortex-M0+)” 1.8.14
  • Librerías Arduino (versiones probadas y fijadas para reproducibilidad):

    • MKRWAN 1.1.0
    • Adafruit BME680 Library 2.0.2
    • Adafruit BusIO 1.14.5 (dependencia de Adafruit BME680)
    • OneWire 2.3.7
    • DallasTemperature 3.11.0
  • Cuenta y aplicación en red LoRaWAN (The Things Stack v3 o compatible):

  • Región/frequency plan (ejemplos): EU868, US915, AU915, AS923, etc.
  • Dispositivo dado de alta en la aplicación (OTAA):

    • DevEUI (lo ideal: leerlo del módem y registrarlo)
    • JoinEUI/AppEUI
    • AppKey
  • Hardware y electricidad:

  • Conocimientos de 3.3 V lógicos (MKR WAN 1310 NO tolera 5 V en GPIO).
  • Sondas y herramientas:

    • Multímetro para verificación de 3.3 V y continuidad.
    • Resistencia 4.7 kΩ para pull‑up en la línea 1‑Wire del DS18B20.
  • Recomendaciones:

  • Editor de texto/código (p. ej., VS Code) con resaltado Arduino.
  • Cable micro‑USB de datos (no solo carga).

Materiales

  • Placa y sensores exactos:
  • Arduino MKR WAN 1310 (ATSAMD21 + CMWX1ZZABZ LoRaWAN)
  • Sensor ambiental BME680 (I2C, dirección típica 0x76)
  • Sensor de temperatura DS18B20 (tubo o encapsulado TO‑92)
  • Componentes y pasivos:
  • 1 × resistencia 4.7 kΩ (pull‑up para línea de datos del DS18B20)
  • Cables Dupont hembra‑macho
  • Protoboard (opcional, recomendado)
  • Alimentación:
  • Cable micro‑USB
  • Batería LiPo 3.7 V (opcional, para pruebas de campo)
  • Red:
  • Pasarela LoRaWAN operativa con cobertura, o acceso a la red pública (TTN) con gateway cercano.

Objetivo del proyecto: construir un nodo “lora‑agro‑microclima‑node” que mida microclima local (temperatura y humedad del aire, presión, resistencia de gas del BME680, temperatura de suelo con DS18B20) y envíe paquetes binarios por LoRaWAN (OTAA) a intervalos configurables.

Preparación y conexión

Instalación de Arduino CLI y toolchain

1) Instala Arduino CLI 1.1.1:
– Linux (x86_64):
– Descarga: https://github.com/arduino/arduino-cli/releases/download/1.1.1/arduino-cli_1.1.1_Linux_64bit.tar.gz
– Extrae y coloca en /usr/local/bin o en tu PATH.
– Windows 11:
– Descarga: arduino-cli_1.1.1_Windows_64bit.zip
– Añade la ruta de arduino-cli.exe al PATH del usuario.
– macOS 14:
– Descarga: arduino-cli_1.1.1_macOS_64bit.zip (o arm64 si aplica)
– Coloca arduino-cli en /usr/local/bin o en /opt/homebrew/bin.

2) Inicializa el entorno (primera vez):
– Crea el archivo de configuración si no existe:
– arduino-cli config init

3) Actualiza el índice e instala el core SAMD exacto:
– arduino-cli core update-index
– arduino-cli core install arduino:samd@1.8.14

4) Instala las librerías con versiones fijadas:
– arduino-cli lib install «MKRWAN@1.1.0»
– arduino-cli lib install «Adafruit BME680 Library@2.0.2»
– arduino-cli lib install «Adafruit BusIO@1.14.5»
– arduino-cli lib install «OneWire@2.3.7»
– arduino-cli lib install «DallasTemperature@3.11.0»

5) Verifica que el FQBN esté disponible:
– arduino-cli board listall | grep mkrwan
– Debe aparecer: arduino:samd:mkrwan1310

Conexiones eléctricas

  • Consideraciones:
  • Todos los módulos comparten GND.
  • Alimentación a 3.3 V desde la MKR WAN 1310.
  • La interfaz I2C del BME680 es 3.3 V; no uses 5 V.
  • El DS18B20 es 3.0–5.5 V; úsalo a 3.3 V en este montaje.
  • Añade pull‑up de 4.7 kΩ entre DATA (DS18B20) y 3.3 V.

Tabla de pines/puertos y cableado:

Módulo/Sensor Señal Pin del sensor Pin en MKR WAN 1310 Notas
BME680 VCC VCC 3V3 3.3 V regulados de la placa
BME680 GND GND GND Tierra común
BME680 SDA (I2C) SDA SDA Pin etiquetado SDA en cabecera MKR
BME680 SCL (I2C) SCL SCL Pin etiquetado SCL en cabecera MKR
DS18B20 VDD VDD 3V3 3.3 V
DS18B20 GND GND GND Tierra común
DS18B20 DATA DQ D2 Línea 1‑Wire; requiere pull‑up de 4.7 kΩ a 3.3 V
Pull‑up 4.7 kΩ DQ—3.3 V Conectar entre DQ (D2) y 3.3 V

Notas:
– En placas MKR, los pines SDA y SCL están claramente etiquetados cerca de AREF. Usa la cabecera marcada (no confundir con D11/D12 en otros form factors).
– La dirección I2C del BME680 suele ser 0x76; si tu breakout usa 0x77, lo ajustaremos en el código.

Preparación de credenciales LoRaWAN

  • Registra un dispositivo OTAA en tu aplicación (TTN o similar).
  • Obtén:
  • JoinEUI/AppEUI (16 hex dígitos)
  • AppKey (32 hex dígitos)
  • DevEUI: puedes leer el DevEUI del módem y registrar ese valor en la consola para evitar errores.

Comprobación de DevEUI desde el propio sketch (lo implementaremos); alternativamente, se puede usar un sketch corto de ejemplo MKRWAN para imprimirlo por Serial.

Código completo (Arduino/C++)

Estructura del proyecto en disco:
– lora-agro-microclima-node/
– lora-agro-microclima-node.ino
– secrets.h

El archivo secrets.h contendrá tus claves OTAA. No lo publiques.

secrets.h (plantilla)

Crea lora-agro-microclima-node/secrets.h con el siguiente contenido y reemplaza las X:

#pragma once
// Claves OTAA (hex ASCII, sin 0x ni espacios)
static const char APP_EUI[] = "0011223344556677"; // JoinEUI/AppEUI, 16 hex
static const char APP_KEY[] = "00112233445566778899AABBCCDDEEFF"; // 32 hex

// Opcional: si conoces el DevEUI de consola y quieres fijarlo,
// de lo contrario, lo leeremos del módem y lo mostraremos.
static const char DEV_EUI_OVERRIDE[] = ""; // deja vacío para usar el del módem

// Región LoRaWAN: usa uno de: EU868, US915, AU915, AS923, IN865, KR920
// Para compilar neutral, usamos define en código; aquí puedes documentar tu plan.

lora-agro-microclima-node.ino

El sketch implementa:
– Inicialización de sensores (BME680 por I2C, DS18B20 en D2).
– Inicialización de módem LoRaWAN (MKRWAN.h) y unión OTAA.
– Empaquetado binario compacto: T_air, RH, P, Gas, T_soil, VBAT.
– Ciclo con envío periódico y backoff si falla la red.

/*
  lora-agro-microclima-node
  Dispositivo: Arduino MKR WAN 1310 + BME680 + DS18B20
  Toolchain: Arduino CLI 1.1.1, SAMD Core 1.8.14, MKRWAN 1.1.0,
             Adafruit BME680 2.0.2, OneWire 2.3.7, DallasTemperature 3.11.0
*/

#include <Arduino.h>
#include <MKRWAN.h>
#include <Wire.h>
#include <Adafruit_BME680.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include "secrets.h"

// Región por compilación: ajusta EU868/US915/AU915/AS923/IN865/KR920
#ifndef LORA_REGION
#define LORA_REGION EU868
#endif

// Pines
static const uint8_t ONE_WIRE_PIN = 2; // D2 para DS18B20

// BME680: dirección por defecto 0x76 (ajusta a 0x77 si tu placa lo requiere)
Adafruit_BME680 bme; // I2C
OneWire oneWire(ONE_WIRE_PIN);
DallasTemperature ds18b20(&oneWire);

// Módem LoRa
LoRaModem modem;

// Configuración
static const uint32_t MEASUREMENT_INTERVAL_MS = 60UL * 1000UL; // 60 s (ajusta)
static const bool USE_CONFIRMED_UPLINK = false; // paquetes no confirmados por defecto
static const uint8_t FPORT = 1;

// Helpers de lectura de batería (ADC AREF = 3.3V, divisor interno del MKR)
float readBatteryVoltage() {
  // En MKR WAN 1310, el pin ADC interno puede leer VBAT a través de un canal dedicado.
  // Para simplificar, podemos usar analogRead(ADC_BATTERY) si está mapeado.
  // Alternativa: si tu core no expone ADC_BATTERY, deja 0.0f o implementa lectura externa.
#ifdef ADC_BATTERY
  uint16_t raw = analogRead(ADC_BATTERY);
  float v = (raw / 1023.0f) * 3.3f * 2.0f; // si hay divisor 1:1 interno (ajustar según placa)
  return v;
#else
  return 0.0f; // placeholder si no está disponible
#endif
}

// Empaquetado binario: escalado fijo
// Layout (12 bytes):
// [0-1]  T_air (°C * 100, int16)
// [2-3]  RH (% * 100, uint16)
// [4-5]  P (hPa * 10, uint16)
// [6-7]  Gas (kΩ * 10, uint16) – recorte a 65535
// [8-9]  T_soil (°C * 100, int16)
// [10-11] Vbat (mV, uint16)
uint16_t clamp_u16(int32_t v) {
  if (v < 0) return 0;
  if (v > 65535) return 65535;
  return (uint16_t)v;
}

void packPayload(uint8_t* buf, size_t len,
                 float t_air, float rh, float p_hpa, float gas_ohms, float t_soil, float vbat) {
  if (len < 12) return;
  int16_t t_air_i16 = (int16_t)roundf(t_air * 100.0f);
  uint16_t rh_u16 = clamp_u16(lroundf(rh * 100.0f));
  uint16_t p_u16 = clamp_u16(lroundf(p_hpa * 10.0f));
  float gas_kohm = gas_ohms / 1000.0f;
  uint16_t gas_u16 = clamp_u16(lroundf(gas_kohm * 10.0f));
  int16_t t_soil_i16 = (int16_t)roundf(t_soil * 100.0f);
  uint16_t vbat_u16 = clamp_u16(lroundf(vbat * 1000.0f));

  buf[0] = (uint8_t)(t_air_i16 >> 8);
  buf[1] = (uint8_t)(t_air_i16 & 0xFF);
  buf[2] = (uint8_t)(rh_u16 >> 8);
  buf[3] = (uint8_t)(rh_u16 & 0xFF);
  buf[4] = (uint8_t)(p_u16 >> 8);
  buf[5] = (uint8_t)(p_u16 & 0xFF);
  buf[6] = (uint8_t)(gas_u16 >> 8);
  buf[7] = (uint8_t)(gas_u16 & 0xFF);
  buf[8] = (uint8_t)(t_soil_i16 >> 8);
  buf[9] = (uint8_t)(t_soil_i16 & 0xFF);
  buf[10] = (uint8_t)(vbat_u16 >> 8);
  buf[11] = (uint8_t)(vbat_u16 & 0xFF);
}

bool initBME680() {
  if (!bme.begin(0x76)) {
    // Intenta en 0x77
    if (!bme.begin(0x77)) {
      Serial.println(F("[BME680] No detectado en 0x76/0x77"));
      return false;
    }
  }
  // Configura oversampling y filtro
  bme.setTemperatureOversampling(BME680_OS_8X);
  bme.setHumidityOversampling(BME680_OS_2X);
  bme.setPressureOversampling(BME680_OS_4X);
  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
  // Habilita gas heater
  bme.setGasHeater(320, 150); // 320°C durante 150 ms
  return true;
}

bool readBME680(float& t, float& h, float& p_hpa, float& gas_ohms) {
  // Realiza lectura forzada
  if (!bme.performReading()) return false;
  t = bme.temperature;          // °C
  h = bme.humidity;             // %
  p_hpa = bme.pressure / 100.0; // Pa -> hPa
  gas_ohms = bme.gas_resistance; // ohmios
  return true;
}

bool readDS18B20(float& t_soil) {
  ds18b20.requestTemperatures();
  float t = ds18b20.getTempCByIndex(0);
  if (t == DEVICE_DISCONNECTED_C) return false;
  t_soil = t;
  return true;
}

bool loraJoinOTAA() {
  Serial.println(F("[LoRa] Inicializando módem..."));
  if (!modem.begin(LORA_REGION)) {
    Serial.println(F("[LoRa] Error al iniciar el módem (begin)"));
    return false;
  }
  // Habilita ADR
  modem.setADR(true);

  // Muestra DevEUI real y permite override si se desea
  String devEUI = modem.deviceEUI();
  Serial.print(F("[LoRa] DevEUI (módem): ")); Serial.println(devEUI);

  if (strlen(DEV_EUI_OVERRIDE) == 16) {
    Serial.print(F("[LoRa] Usando DEV_EUI_OVERRIDE: ")); Serial.println(DEV_EUI_OVERRIDE);
    modem.setDevEUI(DEV_EUI_OVERRIDE);
  }

  // Configura AppEUI y AppKey
  if (strlen(APP_EUI) != 16 || strlen(APP_KEY) != 32) {
    Serial.println(F("[LoRa] APP_EUI/AppKey inválidos (tamaño)."));
    return false;
  }

  // Intenta unión con reintentos exponenciales
  const uint8_t MAX_TRIES = 6;
  uint32_t backoff = 3000; // ms
  for (uint8_t i = 1; i <= MAX_TRIES; ++i) {
    Serial.print(F("[LoRa] joinOTAA intento ")); Serial.println(i);
    if (modem.joinOTAA(APP_EUI, APP_KEY)) {
      Serial.println(F("[LoRa] ¡Unión OTAA exitosa!"));
      return true;
    }
    Serial.print(F("[LoRa] Fallo en join, esperando ")); Serial.print(backoff); Serial.println(F(" ms"));
    delay(backoff);
    backoff = min<uint32_t>(backoff * 2, 120000);
  }
  Serial.println(F("[LoRa] No se pudo unir tras varios intentos."));
  return false;
}

bool loraSend(const uint8_t* payload, size_t len, uint8_t fport, bool confirmed) {
  if (!payload || len == 0) return false;
  int err = modem.beginPacket();
  if (err <= 0) {
    Serial.print(F("[LoRa] beginPacket err=")); Serial.println(err);
    return false;
  }
  modem.write(payload, len);
  // confirmed = true => uplink confirmado; false => no confirmado
  int res = modem.endPacket(confirmed);
  if (res > 0) {
    Serial.print(F("[LoRa] Uplink OK, bytes=")); Serial.println(len);
    // Cambia FPORT si la librería lo soporta por API; si no, envía en port por defecto
    modem.setPort(fport); // en algunas versiones se fija antes de enviar; lo hacemos aquí por compatibilidad
    return true;
  } else {
    Serial.print(F("[LoRa] Uplink FAIL, code=")); Serial.println(res);
    return false;
  }
}

void printHex(const uint8_t* buf, size_t len) {
  for (size_t i = 0; i < len; ++i) {
    if (buf[i] < 16) Serial.print('0');
    Serial.print(buf[i], HEX);
  }
}

void setup() {
  Serial.begin(115200);
  while (!Serial) { ; }
  Serial.println(F("\n[lora-agro-microclima-node] Inicio"));

  // Sensores
  Wire.begin();
  if (!initBME680()) {
    Serial.println(F("[BME680] ERROR inicialización"));
  } else {
    Serial.println(F("[BME680] OK"));
  }
  ds18b20.begin();
  Serial.print(F("[DS18B20] Dispositivos 1-Wire: "));
  Serial.println(ds18b20.getDeviceCount());
  if (!ds18b20.getAddress(NULL, 0)) {
    Serial.println(F("[DS18B20] Atención: no se encontró dirección en índice 0 (puede seguir, pero verifique cableado)"));
  }

  // LoRa
  if (!loraJoinOTAA()) {
    Serial.println(F("[LoRa] No unido. Se reintentará más tarde."));
  }
}

void loop() {
  float t_air = NAN, rh = NAN, p_hpa = NAN, gas_ohms = NAN, t_soil = NAN;
  bool ok_bme = readBME680(t_air, rh, p_hpa, gas_ohms);
  bool ok_ds = readDS18B20(t_soil);
  float vbat = readBatteryVoltage();

  if (!ok_bme) Serial.println(F("[BME680] Lectura fallida"));
  if (!ok_ds)  Serial.println(F("[DS18B20] Lectura fallida"));

  uint8_t payload[12];
  // Valores por defecto si falla lectura
  if (!ok_bme) { t_air = 0; rh = 0; p_hpa = 0; gas_ohms = 0; }
  if (!ok_ds)  { t_soil = 0; }
  if (!(vbat > 0.1f)) vbat = 0.0f;

  packPayload(payload, sizeof(payload), t_air, rh, p_hpa, gas_ohms, t_soil, vbat);

  Serial.print(F("[Payload HEX] "));
  printHex(payload, sizeof(payload));
  Serial.println();

  bool sent = loraSend(payload, sizeof(payload), FPORT, USE_CONFIRMED_UPLINK);
  if (!sent) {
    Serial.println(F("[LoRa] Reintentará unión y envío en próximo ciclo."));
    // Intentar re-unirse si se perdió sesión
    loraJoinOTAA();
  } else {
    // Opción: leer downlink en ventana RX (si librería lo expone)
    if (modem.available()) {
      Serial.print(F("[Downlink] "));
      while (modem.available()) {
        int b = modem.read();
        if (b < 0) break;
        if (b < 16) Serial.print('0');
        Serial.print(b, HEX);
      }
      Serial.println();
    }
  }

  // Espera
  delay(MEASUREMENT_INTERVAL_MS);
}

Puntos clave del código:
– Inicialización BME680: oversampling, filtro y gas heater para lecturas estables.
– DS18B20 en D2 con OneWire; el pull‑up de 4.7 kΩ es obligatorio.
– LoRaWAN: begin(LORA_REGION), setADR(true), joinOTAA con reintentos exponenciales.
– Empaquetado binario compacto de 12 bytes: fácil de decodificar en el backend.
– Envío no confirmado (endPacket(false)) para ahorro de aire y energía; ajustable.

Compilación/flash/ejecución

Asegúrate de que la placa se detecte y toma nota del puerto serie.

1) Detecta la placa y el puerto:
– arduino-cli board list
– Debe listar algo como:
– Port: /dev/ttyACM0 (Linux)
– Port: COM5 (Windows)
– Port: /dev/cu.usbmodemXXX (macOS)
– Board Name: Arduino MKR WAN 1310
– FQBN: arduino:samd:mkrwan1310

2) Compila el proyecto (desde la carpeta que contiene lora-agro-microclima-node):
– arduino-cli compile \
-b arduino:samd:mkrwan1310 \
–warnings all \
–build-property compiler.cpp.extra_flags=»-DLORA_REGION=EU868″ \
lora-agro-microclima-node

Observaciones:
– Cambia -DLORA_REGION=EU868 por tu plan de frecuencias (US915, AU915, etc.).
– Asegúrate de haber creado secrets.h con APP_EUI y APP_KEY válidos.

3) Sube el firmware:
– Linux/macOS:
– arduino-cli upload \
-b arduino:samd:mkrwan1310 \
-p /dev/ttyACM0 \
–verify \
lora-agro-microclima-node
– Windows (ejemplo COM5):
– arduino-cli upload \
-b arduino:samd:mkrwan1310 \
-p COM5 \
–verify \
lora-agro-microclima-node

4) Abre el monitor serie para validar:
– Linux/macOS:
– arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200
– Windows:
– arduino-cli monitor -p COM5 -c baudrate=115200

5) Cambios de región:
– Recompila alterando el flag:
– … –build-property compiler.cpp.extra_flags=»-DLORA_REGION=US915″ …

6) Instalación/actualización del core y librerías (si faltan):
– arduino-cli core update-index
– arduino-cli core install arduino:samd@1.8.14
– arduino-cli lib install «MKRWAN@1.1.0» «Adafruit BME680 Library@2.0.2» «OneWire@2.3.7» «DallasTemperature@3.11.0»

Validación paso a paso

1) Validación eléctrica rápida:
– Con multímetro:
– 3V3 de la MKR: ~3.28–3.32 V.
– Continuidad GND entre sensores y placa.
– Pull‑up de 4.7 kΩ entre D2 y 3.3 V.

2) Validación de detección de placa:
– arduino-cli board list
– Si no aparece, prueba otro cable o puerto USB.

3) Validación de sensores por consola serie:
– Tras reset, debes ver:
– [BME680] OK (o mensaje de error si no detectado)
– [DS18B20] Dispositivos 1-Wire: N (N ≥ 1)
– Cuando hay lectura, se mostrará el payload HEX; por ejemplo:
– [Payload HEX] 07D00FA00E10002A03E807D005DC
– Esto varía según tus mediciones.

4) Validación de unión LoRaWAN:
– Mensajes esperados:
– [LoRa] Inicializando módem…
– [LoRa] DevEUI (módem): XXXXXXXXXXXXXXXX
– [LoRa] joinOTAA intento 1
– [LoRa] ¡Unión OTAA exitosa!
– Si falla, verás reintentos con backoff.

5) Validación de uplink en la consola de la red:
– Abre tu aplicación en The Things Stack (TTN v3).
– En “Live data” del dispositivo, deberías ver uplinks cada ~60 s.
– Payload Length = 12 bytes; Port = 1.

6) Decodificación del payload (servidor):
– Crea un decodificador personalizado con el layout descrito:
– T_air = int16 (big-endian) / 100
– RH = uint16 / 100
– P = uint16 / 10 (hPa)
– Gas = uint16 / 10 (kΩ)
– T_soil = int16 / 100
– Vbat = uint16 (mV)
– Verifica que T_air y T_soil son razonables (20–35 °C según ambiente/suelo), RH (20–90 %),
presión ~ 980–1050 hPa, gas suele fluctuar ampliamente, Vbat según alimentación.

7) Estabilidad:
– Deja el nodo 10–15 minutos:
– Sin pérdida de sesiones (sin rejoin continuos).
– Uplinks regulares a tu intervalo.
– Observa ADR en la red: la tasa de datos podría adaptarse.

Troubleshooting (errores típicos y soluciones)

1) No se detecta el BME680 ([BME680] No detectado en 0x76/0x77)
– Causas:
– Cable SDA/SCL invertido o mal pin.
– Breakout con dirección 0x77; ajusta el begin(0x77).
– Falta de GND común o VCC incorrecto.
– Solución:
– Revisa tabla de pines.
– Prueba ambas direcciones en el código (ya está implementado).
– Ejecuta un I2C scanner para verificar dirección.

2) DS18B20 devuelve DEVICE_DISCONNECTED_C o lectura fallida
– Causas:
– Falta pull‑up de 4.7 kΩ en la línea D2.
– GND/VDD invertidos o cable roto.
– Sensor sumergible con cable demasiado largo sin pull‑up adecuado.
– Solución:
– Añade o verifica la resistencia 4.7 kΩ entre D2 y 3.3 V.
– Usa cables más cortos o baja la frecuencia de sondeo.

3) No aparece el puerto serie en arduino-cli board list
– Causas:
– Cable solo de carga.
– Controladores USB (Windows).
– Puerto bloqueado por otro programa.
– Solución:
– Cambia a un cable de datos.
– Cierra programas que usan el puerto.
– En Windows, actualiza drivers USB nativos (MKR usa CDC estándar, no requiere drivers especiales).

4) joinOTAA falla repetidamente
– Causas:
– Región/banda incorrecta (EU868 vs US915/AU915).
– AppKey/AppEUI con formato o longitud incorrecta.
– Gateway fuera de cobertura o sin backhaul.
– Lista de sub‑bandas en US915/AU915 (TTN usa sub‑band específicas).
– Solución:
– Recompila con -DLORA_REGION adecuado.
– Verifica que APP_EUI = 16 hex y APP_KEY = 32 hex (sin espacios).
– Ubica el nodo cerca del gateway.
– Para US915/AU915, configura sub‑banda si tu librería/firmware lo permite; si no, asegúrate de la compatibilidad del gateway.

5) Uplinks no llegan a la consola, pero el nodo dice “Uplink OK”
– Causas:
– Port incorrecto filtrado por integración.
– Desfase de canales/frecuencias por región.
– RX windows desalineadas (raro si join OK).
– Solución:
– Verifica FPORT=1.
– Asegura misma región en dispositivo y aplicación.
– Re‑join para resincro.

6) Lecturas de gas del BME680 anómalas o lentas en estabilizar
– Causas:
– BME680 requiere “burn‑in” (tiempo de calentamiento) para lecturas significativas de gas.
– Cambios bruscos ambientales.
– Solución:
– Deja el sensor operando ~5–20 minutos para estabilización.
– Evita flujos de aire directos.

7) Vbat siempre 0.0 V
– Causas:
– El macro ADC_BATTERY no está disponible en tu core/placa, o no hay batería conectada.
– Solución:
– Conecta una LiPo a la MKR para lectura real.
– Implementa medición con pin analógico y divisor externo si lo requieres.
– O deja el campo en 0 y evita usarlo en análisis.

8) Error de compilación por librerías/cores en otra versión
– Causas:
– Versiones diferentes a las fijadas.
– Solución:
– Verifica versiones exactas:
– arduino-cli core list
– arduino-cli lib list
– Ajusta con:
– arduino-cli core install arduino:samd@1.8.14
– arduino-cli lib install «MKRWAN@1.1.0» …

Mejoras/variantes

  • Eficiencia energética:
  • Uso de modos de bajo consumo y RTCZero para dormir entre mediciones, reduciendo el consumo a pocos µA.
  • Incrementar el intervalo de envío (5–15 min) según el caso agro, para alargar la batería.

  • Payload y decodificación:

  • Cambiar a CayenneLPP para compatibilidad con plataformas sin decodificador custom.
  • Añadir checksum simple en payload si tu backend lo solicita.

  • Calidad de datos:

  • Integrar la librería BSEC (Bosch) para índices IAQ/VOC/CO2e; requiere más memoria y gestión de licencia/arquitectura.
  • Calibración de sensores (offset de temperatura en DS18B20, validación con termómetro de referencia).

  • LoRaWAN:

  • Confirmed uplink solo para mensajes críticos; activar con USE_CONFIRMED_UPLINK = true.
  • Manejar downlinks para reconfigurar intervalo de medición sobre FPORT 10 (p. ej., 1 byte con minutos).
  • Persistir frame counters y sesión (OTAA) en flash para evitar join frecuente tras reinicios.

  • Hardware:

  • Carcasa IP65 con desecante y filtro sinterizado para BME680 (protección y respuesta de gas).
  • Añadir sensor de humedad de suelo (capacitivo 3.3 V) y pluviómetro de pulsos para un nodo agro más completo.

  • Robustez:

  • Watchdog por software/hardware para recuperación ante bloqueos.
  • Registro de errores en EEPROM/flash para diagnóstico.

Checklist de verificación

  • [ ] Arduino CLI 1.1.1 instalado y en PATH.
  • [ ] Core arduino:samd@1.8.14 instalado.
  • [ ] Librerías instaladas con versiones: MKRWAN 1.1.0, Adafruit BME680 2.0.2, OneWire 2.3.7, DallasTemperature 3.11.0.
  • [ ] Proyecto creado: lora-agro-microclima-node/ con .ino y secrets.h.
  • [ ] APP_EUI (16 hex) y APP_KEY (32 hex) configurados en secrets.h.
  • [ ] BME680 cableado a SDA/SCL, 3V3 y GND.
  • [ ] DS18B20 en D2 con pull‑up 4.7 kΩ a 3.3 V; GND y VDD correctos.
  • [ ] Compilación exitosa con FQBN arduino:samd:mkrwan1310 y región correcta.
  • [ ] Subida exitosa al puerto correcto (/dev/ttyACM0, COMx, etc.).
  • [ ] Consola serie a 115200 bps muestra BME680 OK, conteo DS18B20 y payload HEX.
  • [ ] Unión OTAA exitosa y uplinks visibles en la consola LoRaWAN.
  • [ ] Decodificador en backend interpreta los 12 bytes en unidades correctas.
  • [ ] Ciclo estable durante 10–15 min con uplinks a intervalos regulares.

Apéndice: comandos clave (resumen rápido)

  • Listar placas:
  • arduino-cli board list
  • Actualizar e instalar core:
  • arduino-cli core update-index
  • arduino-cli core install arduino:samd@1.8.14
  • Instalar librerías:
  • arduino-cli lib install «MKRWAN@1.1.0» «Adafruit BME680 Library@2.0.2» «OneWire@2.3.7» «DallasTemperature@3.11.0»
  • Compilar (EU868):
  • arduino-cli compile -b arduino:samd:mkrwan1310 –build-property compiler.cpp.extra_flags=»-DLORA_REGION=EU868″ lora-agro-microclima-node
  • Subir (Linux ejemplo):
  • arduino-cli upload -b arduino:samd:mkrwan1310 -p /dev/ttyACM0 –verify lora-agro-microclima-node
  • Monitor serie:
  • arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

Con esto, dispones de un nodo “lora‑agro‑microclima‑node” fiable en Arduino MKR WAN 1310 que integra BME680 y DS18B20, con toolchain y versiones fijadas, conexiones claras, código reproducible y validación end‑to‑end en LoRaWAN.

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

Ir a Amazon

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

Quiz rápido

Pregunta 1: ¿Cuál es el sistema operativo probado para el uso de Arduino CLI?




Pregunta 2: ¿Qué versión de la librería MKRWAN es la recomendada?




Pregunta 3: ¿Cuál es el voltaje lógico que el MKR WAN 1310 no tolera?




Pregunta 4: ¿Qué tipo de sensor es el BME680?




Pregunta 5: ¿Qué herramienta se recomienda para verificar la continuidad?




Pregunta 6: ¿Qué tipo de conexión se requiere para el sensor DS18B20?




Pregunta 7: ¿Qué tipo de batería es opcional para pruebas de campo?




Pregunta 8: ¿Qué es necesario para dar de alta un dispositivo en la aplicación LoRaWAN?




Pregunta 9: ¿Qué cable se recomienda para la conexión de datos?




Pregunta 10: ¿Cuál es la resistencia recomendada para el pull-up en la línea 1-Wire?




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

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

Sígueme:


Caso práctico: Logger con Arduino Mega 2560, MCP2515 y W5500

Caso práctico: Logger con Arduino Mega 2560, MCP2515 y W5500 — hero

Objetivo y caso de uso

Qué construirás: Un registrador de mantenimiento predictivo utilizando Arduino Mega 2560, MCP2515 y W5500 para monitorear datos en tiempo real.

Para qué sirve

  • Monitoreo de datos de sensores industriales a través de CAN-BUS.
  • Registro de eventos de mantenimiento en una red Ethernet para análisis posterior.
  • Integración de datos de múltiples dispositivos en una única plataforma de visualización.
  • Alertas en tiempo real sobre condiciones anómalas en el sistema.

Resultado esperado

  • Latencia de menos de 100 ms en la transmisión de datos desde el sensor hasta el servidor.
  • Capacidad de registrar hasta 1000 paquetes por segundo desde múltiples sensores.
  • Disponibilidad de datos en tiempo real con una tasa de actualización de 1 segundo.
  • Generación de informes mensuales sobre el estado de los equipos con métricas de uso.

Público objetivo: Ingenieros y técnicos en mantenimiento; Nivel: Avanzado

Arquitectura/flujo: Arduino Mega 2560 -> MCP2515 (CAN-BUS) -> W5500 (Ethernet) -> Servidor de logs.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y herramientas

Este caso práctico ha sido probado en los siguientes entornos. Puedes usar cualquiera de ellos; los comandos se proporcionan para entornos Unix-like y Windows:

  • Ubuntu 22.04 LTS x86_64
  • macOS 14.5 (Sonoma) Apple Silicon/Intel
  • Windows 11 Pro 23H2

Toolchain exacta (versiones)

  • Arduino CLI 0.35.3
  • Core “Arduino AVR Boards” arduino:avr@1.8.6
  • FQBN objetivo: arduino:avr:mega (Arduino Mega 2560)
  • Bibliotecas Arduino:
  • Ethernet@2.0.2 (compatible W5500)
  • mcp_can@1.5.1 (Cory J. Fowler, para MCP2515)
  • Dependencias del core: SPI (incluida con arduino:avr)
  • Utilidades opcionales de validación (en el PC):
  • can-utils 2021.08 (cangen/cansend/candump) en Linux
  • netcat (nc 1.206) o ncat 7.94 para UDP en el servidor receptor de logs

Nota: Usaremos Arduino CLI (no GUI). Ajustaremos el FQBN a “arduino:avr:mega” para el Arduino Mega 2560.

Materiales

  • Arduino Mega 2560 (modelo exacto requerido)
  • Seeed CAN-BUS Shield V2 (MCP2515 + TJA1050, cristal 16 MHz)
  • W5500 Ethernet Shield (formato R3, con conector ICSP y microSD)
  • Cable USB A-B para el Mega 2560
  • Cable Ethernet UTP Cat5e o superior
  • Par trenzado CAN para conexión al bus (CAN_H, CAN_L)
  • Terminación 120 Ω (si el shield/instalación lo requiere y estás en el extremo del bus)
  • Acceso a una red local con servidor UDP para recolectar logs (puede ser un PC)
  • Opcional: interfaz USB–CAN en el PC para inyectar tramas de prueba

Objetivo del proyecto: construir un “can-predictive-maintenance-logger” que lea señales de un bus CAN industrial (500 kbit/s), calcule métricas de condición (media móvil, desviación estándar, EWMA, detección de anomalías por Z-score) y publique resúmenes y eventos por UDP/JSON a un servidor en la red mediante el W5500.

Preparación y conexión

Pilas de shields y pines SPI/CS

Usaremos dos dispositivos SPI simultáneamente sobre el Arduino Mega 2560:

  • MCP2515 (CAN) en el Seeed CAN-BUS Shield V2
  • CS predeterminado: D9
  • INT: D2
  • Reloj del MCP2515: 16 MHz (importante para configurar el bitrate)
  • W5500 (Ethernet Shield)
  • CS: D10
  • MicroSD (no usado en este proyecto): CS D4

El Mega 2560 no expone SPI en los pines D11–D13 como el UNO; el SPI está en el cabezal ICSP. Ambos shields en formato R3 bien diseñados usan el conector ICSP, por lo que son compatibles al apilarlos.

Asegúrate de:

  • Colocar ambos shields de forma que tomen el SPI del conector ICSP.
  • Dejar CS de cada dispositivo en “HIGH” cuando no se use (evita colisiones de bus).
  • Configurar D4 (SD del Ethernet shield) como salida y en HIGH para que no interfiera.

Tabla de conexiones y parámetros

Función/Señal Shield/Componente Pin Arduino Mega 2560 Notas
SPI SCK/MOSI/MISO ICSP (ambos shields) ICSP Compartido por W5500 y MCP2515
CS Ethernet (W5500) Ethernet Shield D10 Ethernet.init(10) en código
CS CAN (MCP2515) Seeed CAN-BUS Shield V2 D9 MCP_CAN(9) en código
INT CAN Seeed CAN-BUS Shield V2 D2 Entrada de interrupción
CS MicroSD (no usado) Ethernet Shield D4 Mantener HIGH
CAN_H Seeed CAN-BUS Shield V2 Conectar a CAN_H del bus
CAN_L Seeed CAN-BUS Shield V2 Conectar a CAN_L del bus
Terminación CAN 120 Ω Seeed Shield (jumper) o externa Activar solo si es extremo de la línea
Ethernet RJ45 W5500 Conectar a switch/router local

Topología de red y CAN

  • Velocidad CAN del proyecto: 500 kbit/s.
  • Oscilador MCP2515: 16 MHz.
  • Ethernet: IP estática para el logger (por ejemplo 192.168.1.50/24), gateway 192.168.1.1.
  • Servidor UDP recolector: 192.168.1.100:5140 (ajústalo en el código si necesitas otros valores).

Código completo (Arduino C++)

El siguiente sketch implementa:

  • Inicialización de SPI, CAN (mcp_can) y Ethernet (W5500).
  • Sincronización NTP básica para timestamp en segundos UNIX.
  • Filtros CAN para tres tramas ejemplo (IDs estándar 0x301, 0x302, 0x303).
  • Extracción de señales:
  • 0x301: temperatura en centi-grados (int16 LE)
  • 0x302: vibración RMS en milli-g (uint16 LE)
  • 0x303: corriente en centi-amperios (uint16 LE)
  • Cálculo de media y varianza incremental (Welford), EWMA y Z-score.
  • Publicación por UDP/JSON cada LOG_INTERVAL_MS y generación de eventos de anomalía.

Copia el código en un archivo: src/can_predictive_maintenance_logger.ino o un .ino con el mismo nombre del directorio.

/*
  can_predictive_maintenance_logger.ino
  Dispositivo: Arduino Mega 2560 + Seeed CAN-BUS Shield V2 (MCP2515+TJA1050) + W5500 Ethernet Shield
  Toolchain: Arduino CLI 0.35.3, arduino:avr@1.8.6, Ethernet@2.0.2, mcp_can@1.5.1
*/

#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include "mcp_can.h"

// ----------------------- Configuración de pines y constantes -----------------------
static const uint8_t PIN_CS_ETH   = 10; // W5500 CS
static const uint8_t PIN_CS_CAN   = 9;  // MCP2515 CS (Seeed CAN-BUS Shield v2)
static const uint8_t PIN_CS_SD    = 4;  // MicroSD en Ethernet Shield (no usado)
static const uint8_t PIN_CAN_INT  = 2;  // Interrupción MCP2515

// CAN bitrate y reloj del MCP2515 (Seeed v2 usa 16 MHz)
static const uint8_t CAN_BITRATE  = CAN_500KBPS; // de mcp_can.h
static const uint8_t CAN_CLK      = MCP_16MHZ;   // de mcp_can.h

// Filtrado de IDs (11-bit estándar)
static const uint16_t ID_TEMP     = 0x301; // int16 LE, centi-grados Celsius
static const uint16_t ID_VIBR     = 0x302; // uint16 LE, milli-g RMS
static const uint16_t ID_CURR     = 0x303; // uint16 LE, centi-amperios

// Intervalo de logging
static const unsigned long LOG_INTERVAL_MS = 1000; // 1 s

// Parámetros EWMA y detección
static const float ALPHA_EWMA = 0.10f;      // suavizado
static const float Z_THRESHOLD = 3.0f;      // anomalía si |z| > 3
static const int   ANOM_CONSEC = 3;         // eventos consecutivos para "warning"

// Ethernet (IP estática)
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEE, 0xFE, 0xED };
IPAddress ip(192, 168, 1, 50);
IPAddress dns(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);

// Destino UDP (servidor de logs)
IPAddress remote(192, 168, 1, 100);
const uint16_t remotePort = 5140;
EthernetUDP Udp;

// NTP (simple)
IPAddress ntpServerIP;
const char* ntpServerName = "pool.ntp.org";
const unsigned int localNtpPort = 2390;
const int NTP_PACKET_SIZE = 48;
byte ntpBuffer[NTP_PACKET_SIZE];
unsigned long unixTimeBase = 0;  // epoch de referencia
unsigned long ntpLastSyncMs = 0;
const unsigned long NTP_RESYNC_MS = 3600000UL; // 1 hora

// Instancia MCP2515
MCP_CAN CAN0(PIN_CS_CAN);

// ----------------------- Estado y estadísticas -----------------------
enum ChannelIndex { CH_TEMP = 0, CH_VIBR = 1, CH_CURR = 2, CH_N = 3 };

struct Stats {
  uint32_t n;
  double mean;
  double M2;    // sumatoria para varianza
  double ewma;
  int consecAnom;
  bool initialized;
};

Stats stats[CH_N];

unsigned long lastLogMs = 0;

// ----------------------- Utilidades -----------------------
static inline void sd_disable() {
  pinMode(PIN_CS_SD, OUTPUT);
  digitalWrite(PIN_CS_SD, HIGH);
}

static inline void can_disable() {
  pinMode(PIN_CS_CAN, OUTPUT);
  digitalWrite(PIN_CS_CAN, HIGH);
}

static inline void eth_disable() {
  pinMode(PIN_CS_ETH, OUTPUT);
  digitalWrite(PIN_CS_ETH, HIGH);
}

void stats_init(Stats &s) {
  s.n = 0;
  s.mean = 0.0;
  s.M2 = 0.0;
  s.ewma = 0.0;
  s.consecAnom = 0;
  s.initialized = false;
}

void stats_update(Stats &s, double x) {
  // Welford para media y varianza
  s.n++;
  double delta = x - s.mean;
  s.mean += delta / (double)s.n;
  double delta2 = x - s.mean;
  s.M2 += delta * delta2;

  // EWMA
  if (!s.initialized) {
    s.ewma = x;
    s.initialized = true;
  } else {
    s.ewma = (ALPHA_EWMA * x) + (1.0 - ALPHA_EWMA) * s.ewma;
  }
}

double stats_var(const Stats &s) {
  if (s.n < 2) return 0.0;
  return s.M2 / (double)(s.n - 1);
}

double stats_std(const Stats &s) {
  double v = stats_var(s);
  return v > 0 ? sqrt(v) : 0.0;
}

double stats_zscore(const Stats &s, double x) {
  double sd = stats_std(s);
  if (sd <= 1e-12) return 0.0;
  return (x - s.mean) / sd;
}

unsigned long nowEpoch() {
  if (unixTimeBase == 0) return 0; // no sincronizado
  // Convertir millis transcurridos desde la sincronización a segundos
  return unixTimeBase + (millis() - ntpLastSyncMs) / 1000UL;
}

// NTP: prepara y envía un paquete de consulta
void sendNTPpacket(IPAddress &address) {
  memset(ntpBuffer, 0, NTP_PACKET_SIZE);
  ntpBuffer[0] = 0b11100011;   // LI, Version, Mode
  ntpBuffer[1] = 0;            // Stratum, or type of clock
  ntpBuffer[2] = 6;            // Polling Interval
  ntpBuffer[3] = 0xEC;         // Peer Clock Precision
  // bytes 12 hasta 15 para la marca de tiempo
  ntpBuffer[12]  = 49;
  ntpBuffer[13]  = 0x4E;
  ntpBuffer[14]  = 49;
  ntpBuffer[15]  = 52;

  Udp.beginPacket(address, 123); // NTP usa puerto 123
  Udp.write(ntpBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

bool ntpSync() {
  if (Ethernet.hostByName(ntpServerName, ntpServerIP) != 1) {
    return false;
  }
  sendNTPpacket(ntpServerIP);
  delay(1000);

  int size = Udp.parsePacket();
  if (size >= NTP_PACKET_SIZE) {
    Udp.read(ntpBuffer, NTP_PACKET_SIZE);
    // Los segundos NTP empiezan en 1900; UNIX en 1970.
    unsigned long highWord = word(ntpBuffer[40], ntpBuffer[41]);
    unsigned long lowWord  = word(ntpBuffer[42], ntpBuffer[43]);
    unsigned long secsSince1900 = (highWord << 16) | lowWord;
    const unsigned long seventyYears = 2208988800UL;
    unsigned long epoch = secsSince1900 - seventyYears;

    unixTimeBase = epoch;
    ntpLastSyncMs = millis();
    return true;
  }
  return false;
}

void logUdpJson(const char* json) {
  Udp.beginPacket(remote, remotePort);
  Udp.write((const uint8_t*)json, strlen(json));
  Udp.endPacket();
}

void publish_summary() {
  unsigned long ts = nowEpoch();
  char buf[512];

  // Construir JSON compacto (sin ArduinoJson para reducir dependencias)
  // Ejemplo:
  // {"ts":1700000000,"src":"mega2560-can-logger","ch":[{"k":"temp","n":100,"mean":..,"std":..,"ewma":..},{"k":"vibr",...},{"k":"curr",...}]}
  snprintf(buf, sizeof(buf),
    "{\"ts\":%lu,\"src\":\"mega2560-can-logger\",\"ch\":["
      "{\"k\":\"temp\",\"n\":%lu,\"mean\":%.3f,\"std\":%.3f,\"ewma\":%.3f},"
      "{\"k\":\"vibr\",\"n\":%lu,\"mean\":%.3f,\"std\":%.3f,\"ewma\":%.3f},"
      "{\"k\":\"curr\",\"n\":%lu,\"mean\":%.3f,\"std\":%.3f,\"ewma\":%.3f}"
    "]}",
    ts,
    (unsigned long)stats[CH_TEMP].n, stats[CH_TEMP].mean, stats_std(stats[CH_TEMP]), stats[CH_TEMP].ewma,
    (unsigned long)stats[CH_VIBR].n, stats[CH_VIBR].mean, stats_std(stats[CH_VIBR]), stats[CH_VIBR].ewma,
    (unsigned long)stats[CH_CURR].n, stats[CH_CURR].mean, stats_std(stats[CH_CURR]), stats[CH_CURR].ewma
  );

  logUdpJson(buf);
  Serial.println(buf);
}

void publish_anomaly(const char* key, double x, double z) {
  unsigned long ts = nowEpoch();
  char buf[256];
  snprintf(buf, sizeof(buf),
    "{\"ts\":%lu,\"src\":\"mega2560-can-logger\",\"evt\":\"anomaly\",\"k\":\"%s\",\"x\":%.3f,\"z\":%.3f}",
    ts, key, x, z
  );
  logUdpJson(buf);
  Serial.println(buf);
}

void handle_channel(ChannelIndex ch, double value) {
  const char* key = (ch == CH_TEMP) ? "temp" : (ch == CH_VIBR) ? "vibr" : "curr";
  stats_update(stats[ch], value);
  double z = stats_zscore(stats[ch], value);
  if (fabs(z) > Z_THRESHOLD) {
    stats[ch].consecAnom++;
    publish_anomaly(key, value, z);
  } else {
    stats[ch].consecAnom = 0;
  }
  if (stats[ch].consecAnom >= ANOM_CONSEC) {
    // Estado de "warning": enviamos un evento especial
    char buf[256];
    unsigned long ts = nowEpoch();
    snprintf(buf, sizeof(buf),
      "{\"ts\":%lu,\"src\":\"mega2560-can-logger\",\"evt\":\"warning\",\"k\":\"%s\",\"consec\":%d}",
      ts, key, stats[ch].consecAnom
    );
    logUdpJson(buf);
    Serial.println(buf);
    stats[ch].consecAnom = 0; // rearmar
  }
}

// Lectura de frame y decodificación según IDs del ejemplo
void process_can() {
  unsigned long rxId;
  byte len = 0;
  byte buf[8];

  // Usamos la línea INT para minimizar polling, pero verificamos buffer disponible
  if (digitalRead(PIN_CAN_INT) == LOW || CAN0.checkReceive() == CAN_MSGAVAIL) {
    if (CAN0.readMsgBuf(&rxId, &len, buf) == CAN_OK) {
      bool ext = (rxId & 0x80000000UL); // librería coloca flag en bit 31 si extendido
      if (ext) return; // ignorar extendidos en este ejemplo

      uint16_t sid = (uint16_t)(rxId & 0x7FF);

      if (sid == ID_TEMP && len >= 2) {
        int16_t raw = (int16_t)(buf[0] | (buf[1] << 8)); // LE
        double tempC = raw / 100.0; // centi-grados -> °C
        handle_channel(CH_TEMP, tempC);
      } else if (sid == ID_VIBR && len >= 2) {
        uint16_t raw = (uint16_t)(buf[0] | (buf[1] << 8)); // LE
        double vibrG = raw / 1000.0; // milli-g -> g
        handle_channel(CH_VIBR, vibrG);
      } else if (sid == ID_CURR && len >= 2) {
        uint16_t raw = (uint16_t)(buf[0] | (buf[1] << 8)); // LE
        double currA = raw / 100.0; // centi-amp -> A
        handle_channel(CH_CURR, currA);
      }
    }
  }
}

bool can_setup_filters() {
  // Configuramos máscaras y filtros para recibir solo 0x301, 0x302, 0x303
  // MCP2515 tiene 2 máscaras y 6 filtros.
  // Máscara 0: match exacto (0x7FF)
  if (CAN0.init_Mask(0, 0, 0x7FF) != CAN_OK) return false; // RXM0
  if (CAN0.init_Filt(0, 0, ID_TEMP) != CAN_OK) return false; // RXF0
  if (CAN0.init_Filt(1, 0, ID_VIBR) != CAN_OK) return false; // RXF1

  // Máscara 1: match exacto (0x7FF)
  if (CAN0.init_Mask(1, 0, 0x7FF) != CAN_OK) return false; // RXM1
  if (CAN0.init_Filt(2, 0, ID_CURR) != CAN_OK) return false; // RXF2
  // Resto de filtros no usados
  if (CAN0.init_Filt(3, 0, 0) != CAN_OK) return false;
  if (CAN0.init_Filt(4, 0, 0) != CAN_OK) return false;
  if (CAN0.init_Filt(5, 0, 0) != CAN_OK) return false;

  return true;
}

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

  // Asegurar CS de todos los dispositivos no usados en HIGH
  sd_disable();
  can_disable();
  eth_disable();

  // Ethernet W5500
  Ethernet.init(PIN_CS_ETH);        // CS W5500 = D10
  Ethernet.begin(mac, ip, dns, gateway, subnet);
  delay(1000);
  Udp.begin(8888); // puerto local UDP para NTP y opcional

  // CAN MCP2515
  pinMode(PIN_CAN_INT, INPUT);
  if (CAN0.begin(MCP_ANY, CAN_BITRATE, CAN_CLK) != CAN_OK) {
    Serial.println(F("ERROR: CAN init falló"));
    while (1) { delay(100); }
  }
  if (!can_setup_filters()) {
    Serial.println(F("ERROR: Filtros CAN fallaron"));
    while (1) { delay(100); }
  }
  CAN0.setMode(MCP_NORMAL); // modo normal (no loopback)

  // Inicializar estadísticas
  for (int i = 0; i < CH_N; ++i) stats_init(stats[i]);

  // Sincronización NTP (mejor esfuerzo)
  if (ntpSync()) {
    Serial.println(F("NTP sincronizado"));
  } else {
    Serial.println(F("ADVERTENCIA: NTP no sincronizado, se usarán timestamps 0"));
  }

  lastLogMs = millis();
  Serial.println(F("Inicio OK: can-predictive-maintenance-logger"));
}

// ----------------------- Loop -----------------------
void loop() {
  // Procesa frames CAN disponibles
  process_can();

  // Re-sync NTP de forma periódica
  if (millis() - ntpLastSyncMs > NTP_RESYNC_MS) {
    ntpSync();
  }

  // Publicación periódica
  if (millis() - lastLogMs >= LOG_INTERVAL_MS) {
    publish_summary();
    lastLogMs = millis();
  }
}

Explicación breve de las partes clave

  • Inicialización SPI/CS: fijamos CS de SD, CAN y ETH en HIGH antes de inicializar para evitar que algún dispositivo “se cuelgue” el bus.
  • Ethernet.init(10) y Ethernet.begin(mac, ip, …): asegura que la librería hable con el W5500 (CS D10) usando IP estática, necesario para un logger estable.
  • MCP_CAN CAN0(9): fija el pin CS del MCP2515 en D9 (por convención de Seeed CAN-BUS Shield V2).
  • can_setup_filters(): crea dos máscaras y filtros en el MCP2515 para aceptar únicamente tres IDs de ejemplo (0x301, 0x302, 0x303), reduciendo carga de CPU y ruido de bus.
  • Estadísticos (Welford + EWMA): ofrecen medias y desviaciones robustas a lo largo del tiempo. Z-score define anomalías si |z| > 3.
  • NTP: una implementación mínima vía UDP para timestamp UNIX; si falla, se publican timestamps 0 (el servidor puede asignar hora de recepción).
  • Publicación UDP/JSON: envía resúmenes cada segundo y eventos de anomalía inmediatamente.

Compilación, flash y ejecución (Arduino CLI)

Asegúrate de tener conectada la placa “Arduino Mega 2560” por USB y de conocer el puerto serie (COMx en Windows, /dev/ttyACM0 o /dev/ttyUSB0 en Linux, /dev/cu.usbmodem* en macOS).

1) Instalar Arduino CLI 0.35.3 (si no lo tienes)

  • Linux/macOS (bash):
# Descargar e instalar (ajusta arquitectura si procede)
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/v0.35.3/install.sh | BINDIR=$HOME/.local/bin sh
~/.local/bin/arduino-cli version
  • Windows (PowerShell):
iwr https://downloads.arduino.cc/arduino-cli/arduino-cli_0.35.3_Windows_64bit.zip -OutFile arduino-cli.zip
Expand-Archive arduino-cli.zip -DestinationPath $env:USERPROFILE\arduino-cli
$env:Path += ";$env:USERPROFILE\arduino-cli"
arduino-cli.exe version

2) Preparar el core y las bibliotecas exactas

arduino-cli core update-index
arduino-cli core install arduino:avr@1.8.6

# Bibliotecas exactas
arduino-cli lib install Ethernet@2.0.2
arduino-cli lib install mcp_can@1.5.1

3) Verificar puerto y FQBN

arduino-cli board list
# Identifica tu Mega 2560 y su puerto, por ejemplo: /dev/ttyACM0 o COM5

arduino-cli board attach -p /dev/ttyACM0 -b arduino:avr:mega .

4) Estructura de proyecto y compilación

Se recomienda esta estructura:

  • Proyecto/
  • src/can_predictive_maintenance_logger.ino

Compila:

arduino-cli compile --fqbn arduino:avr:mega ./Proyecto

5) Subida (flash)

# Linux/macOS
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega ./Proyecto

# Windows (ejemplo COM5)
arduino-cli upload -p COM5 --fqbn arduino:avr:mega .\Proyecto

6) Monitor serie (para ver JSON localmente)

# Linux/macOS
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

# Windows
arduino-cli monitor -p COM5 -c baudrate=115200

El dispositivo arrancará, intentará NTP, inicializará CAN y empezará a emitir resúmenes por UDP cada segundo y eventos de anomalía al vuelo.

Validación paso a paso

Hay dos partes a validar: recepción CAN y publicación UDP.

1) Validar la salida UDP/JSON

En el servidor recolector (IP 192.168.1.100 en este ejemplo):

  • Linux/macOS:
nc -ul 5140
# o con ncat
ncat -ul 5140
  • Windows (ncat de Nmap):
ncat.exe -ul 5140

Deberías ver JSON como:

  • Resumen periódico:
{"ts":1700000000,"src":"mega2560-can-logger","ch":[{"k":"temp","n":12,"mean":42.317,"std":0.520,"ewma":42.210},{"k":"vibr","n":12,"mean":0.112,"std":0.014,"ewma":0.110},{"k":"curr","n":12,"mean":3.242,"std":0.083,"ewma":3.201}]}
  • Evento de anomalía:
{"ts":1700000001,"src":"mega2560-can-logger","evt":"anomaly","k":"vibr","x":0.350,"z":3.800}
  • Evento de warning (consecutivas):
{"ts":1700000003,"src":"mega2560-can-logger","evt":"warning","k":"vibr","consec":3}

Si el NTP no se sincroniza, ts puede ser 0; la hora de recepción en el servidor te servirá para validar el flujo.

2) Validar recepción CAN

Si dispones de un adaptador USB–CAN con SocketCAN en Linux:

  1. Prepara la interfaz a 500 kbit/s:
sudo ip link set can0 down 2>/dev/null || true
sudo ip link add dev can0 type can bitrate 500000
sudo ip link set can0 up
  1. Envía tramas de ejemplo (IDs del proyecto):
# 0x301: temperatura: 42.35 °C -> 4235 (0x108B) LE = 8B 10
cansend can0 301#8B10

# 0x302: vibración: 0.115 g -> 115 milli-g = 0x0073 LE = 73 00
cansend can0 302#7300

# 0x303: corriente: 3.20 A -> 320 centi-A = 0x0140 LE = 40 01
cansend can0 303#4001
  1. Observa en el monitor serie y en el servidor UDP cómo cambian las estadísticas y si se generan eventos cuando empujas valores extremos, por ejemplo:
# Provoca anomalía de vibración: 0.5 g
cansend can0 302#F401

Si no tienes SocketCAN:

  • Puedes usar un generador de tramas del fabricante de tu interfaz USB–CAN a 500 kbit/s con los mismos IDs.
  • Comprueba que el shield esté con terminación 120 Ω activada solo si estás en el extremo del bus.

3) Validar filtros CAN

Envía tramas con IDs no listadas (por ejemplo 0x100, 0x7FF). No deberían afectar ni contar en las estadísticas (n no cambia). Esto verifica que el filtrado en MCP2515 está activo.

4) Validar NTP

Temporalmente desconecta la red y reinicia el Arduino: verás “ADVERTENCIA: NTP no sincronizado” en el monitor serie y timestamps 0 en JSON. Reestablece la red; pasado 1 hora (o forzando una resync manual cambiando NTP_RESYNC_MS a un valor menor), ts empezará a ser no nulo.

Troubleshooting

1) El W5500 no obtiene link (LEDs del RJ45 apagados)
– Causas: cable Ethernet defectuoso, puerto del switch muerto, falta de alimentación.
– Solución: cambia el puerto y el cable; verifica que Ethernet.hardwareStatus() devuelva EthernetNoHardware vs EthernetW5500 si añades un print de diagnóstico; confirma que Ethernet.init(10) coincide con tu CS.

2) El CAN no recibe nada
– Causas: bitrate incorrecto, cristal mal configurado, no hay terminación, polaridad de H/L invertida, INT no conectado.
– Solución: verifica que CAN_500KBPS y MCP_16MHZ coinciden con tu hardware (Seeed V2 usa 16 MHz); comprueba que el bus tiene terminadores 120 Ω en ambos extremos; asegúrate de usar CAN_H a H y CAN_L a L.

3) Choque en SPI entre W5500 y MCP2515
– Síntomas: lecturas CAN erráticas al usar Ethernet o viceversa.
– Solución: asegúrate de que D4 (SD) está como salida HIGH; define CS de los dispositivos no usados en HIGH antes de inicializar; revisa que los shields usen el conector ICSP en el Mega.

4) “ERROR: CAN init falló” en el arranque
– Causas: CS incorrecto, shield mal apilado, alimentación insuficiente, MCP2515 no presente.
– Solución: confirma PIN_CS_CAN = 9; cambia el orden físico de apilado si el pin 9 está “tapado”; prueba a alimentar el conjunto con una fuente externa estable si hay otros periféricos.

5) No llegan logs UDP al servidor
– Causas: IP mal configurada, conflicto de IP, firewall bloqueando UDP/5140.
– Solución: haz ping a la IP del logger; comprueba que el servidor está escuchando en 0.0.0.0:5140; temporalmente desactiva el firewall o crea una regla de entrada para UDP/5140.

6) NTP nunca sincroniza
– Causas: DNS inaccesible, puerto UDP/123 bloqueado, sin salida a Internet.
– Solución: usa un servidor NTP local conocido y reemplaza pool.ntp.org por su IP; valida con Ethernet.hostByName que resuelva; si no se requiere sello horario estricto, tolera ts=0.

7) Desbordamiento de JSON o truncamiento
– Síntomas: líneas cortadas en el servidor.
– Causas: buffer pequeño.
– Solución: incrementa el tamaño de buf en publish_summary/publish_anomaly si agregas más campos.

8) Recepción de tramas extendidas inesperadas
– Síntomas: estadísticas cambian con IDs no previstas.
– Solución: mantén en false los extendidos; el código ya descarta tramas con bit extendido; refuerza máscaras/filters para 11-bit.

Mejoras/variantes

  • Persistencia local en microSD: habilita CS D4 para registrar CSV/JSON cuando no haya red. Cambia sd_disable por inicialización de SD (SdFat o SD) y asegúrate de arbitrar CS con W5500.
  • Publicación a InfluxDB/HTTP: cambia UDP por HTTP POST a /write?db=… usando EthernetClient; format line protocol; considera backoff y cola local.
  • CAN extendido y protocolos: adapta a J1939 (29-bit) o CANopen; amplía filtros a PGNs específicos; añade decodificación de SPNs.
  • Configuración por DHCP y mDNS: usa Ethernet.begin(mac) con DHCP; anuncia servicio via mDNS (se requiere librería MDNS compatible con W5500).
  • Ventanas temporales: en vez de estadísticas globales, implementa ventana deslizante fija (por ejemplo 5 min) con buffer circular por canal.
  • Modelos de anomalía más avanzados: incorpora Holt-Winters, percentiles, LOF simplificado, o umbrales adaptativos por estado de operación.
  • Buffer de eventos y reintentos: en caso de fallo de red, almacena en RAM/SD y reintenta durante X periodos.
  • Telemetría de salud del nodo: añade métricas como freeRAM (utilidad de SRAM disponível), latencias de lectura CAN, contadores de frames descartados.

Checklist de verificación

  • [ ] Herramientas instaladas:
  • [ ] Arduino CLI 0.35.3
  • [ ] Core arduino:avr@1.8.6
  • [ ] Bibliotecas Ethernet@2.0.2 y mcp_can@1.5.1
  • [ ] Hardware correcto: Arduino Mega 2560 + Seeed CAN-BUS Shield V2 + W5500 Ethernet Shield
  • [ ] Conexiones:
  • [ ] Shields apilados usando conector ICSP
  • [ ] CS W5500 en D10, MCP2515 en D9, SD en D4 (HIGH)
  • [ ] INT MCP2515 a D2
  • [ ] CAN_H/CAN_L conectados con terminación según topología
  • [ ] Ethernet RJ45 conectado a red local
  • [ ] Configuración de red: IP del logger sin conflicto, servidor UDP escuchando en 192.168.1.100:5140
  • [ ] Compilación y subida con:
  • [ ] arduino-cli compile –fqbn arduino:avr:mega
  • [ ] arduino-cli upload -p –fqbn arduino:avr:mega
  • [ ] Validación:
  • [ ] Se observan resúmenes JSON por UDP cada 1 s
  • [ ] Inyectando tramas 0x301/0x302/0x303 cambian las estadísticas
  • [ ] Valores extremos generan eventos “anomaly” y “warning”
  • [ ] Filtros: tramas con otros IDs no afectan las métricas
  • [ ] Estabilidad:
  • [ ] Sin colisiones SPI (SD desactivada, CS correctos)
  • [ ] Link Ethernet activo (LEDs en RJ45)
  • [ ] CAN estable (sin errores de bus visibles)

Con este flujo, dispones de un logger de mantenimiento predictivo sobre CAN que calcula métricas en tiempo real y exporta telemetría por Ethernet usando exclusivamente “Arduino Mega 2560 + Seeed CAN-BUS Shield V2 (MCP2515+TJA1050) + W5500 Ethernet Shield” y la toolchain especificada, listo para integrarse en pipelines de análisis y alertado en tu red.

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 modelo exacto del Arduino requerido para este proyecto?




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




Pregunta 3: ¿Qué biblioteca se utiliza para la comunicación CAN?




Pregunta 4: ¿Qué tipo de cable se necesita para la conexión al bus CAN?




Pregunta 5: ¿Cuál es la velocidad del bus CAN utilizada en este proyecto?




Pregunta 6: ¿Qué herramienta se menciona para validar en el PC?




Pregunta 7: ¿Qué tipo de red se necesita para recolectar logs?




Pregunta 8: ¿Qué componente se utiliza para la comunicación Ethernet?




Pregunta 9: ¿Qué métrica NO se menciona como parte del cálculo en el proyecto?




Pregunta 10: ¿Qué se debe ajustar para usar el Arduino Mega 2560?




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

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

Sígueme:


Caso práctico: MJPEG con cámara CSI en Raspberry Pi Zero W

Caso práctico: MJPEG con cámara CSI en Raspberry Pi Zero W — hero

Objetivo y caso de uso

Qué construirás: Un servidor web MJPEG que transmite video en tiempo real desde una Raspberry Pi Zero W con Camera Module v2.

Para qué sirve

  • Transmisión de video en tiempo real para monitoreo remoto de espacios.
  • Integración en sistemas de domótica para visualización de cámaras de seguridad.
  • Proyectos de robótica donde se requiere streaming de video en vivo.
  • Aplicaciones de telemedicina que permiten la supervisión visual de pacientes.

Resultado esperado

  • Latencia de transmisión menor a 100 ms.
  • Capacidad de manejar al menos 5 conexiones simultáneas sin pérdida de calidad.
  • FPS (fotogramas por segundo) estables en 30 FPS durante la transmisión.
  • Consumo de ancho de banda de aproximadamente 1.5 Mbps por flujo de video.

Público objetivo: Desarrolladores y entusiastas de la tecnología; Nivel: Avanzado

Arquitectura/flujo: Raspberry Pi Zero W con Camera Module v2 -> Servidor Flask -> Transmisión MJPEG a través de HTTP.

Nivel: Avanzado

Prerrequisitos

Este caso práctico está verificado y documentado específicamente para el modelo “Raspberry Pi Zero W + Camera Module v2 (IMX219)”, orientado a montar un servidor web que publique un stream MJPEG (multipart/x-mixed-replace) vía CSI. Se asume que trabajas con la pila actual basada en libcamera (sin el stack “legacy”).

  • Sistema operativo
  • Raspberry Pi OS Bookworm Lite (32-bit, armhf). El Pi Zero W no soporta 64-bit; Bookworm 32-bit incluye Python 3.11 por defecto.
  • Imagen recomendada (ejemplo): 2024-10-22 o posterior.
  • Kernel Linux: 6.6.x (cualquiera de la serie estable incluida con la imagen anterior).
  • Toolchain y versiones probadas (recomendadas para reproducibilidad)
  • Python: 3.11.2 (Bookworm)
  • Pip: 23.2.1
  • Venv: 3.11 (módulo estándar)
  • GCC: 12.2.0 (Debian 12)
  • CMake: 3.25.1
  • libcamera: 0.0.5+ (paquetes de Raspberry Pi OS Bookworm; ver sección de validación para comprobar versión local)
  • libcamera-apps: 0.0.5+ (rpi)
  • python3-picamera2: 0.3.16–0.3.17 (del repo Raspberry Pi OS)
  • Flask: 2.3.3 (pip)
  • Pillow: 10.3.0 (pip, vía piwheels)
  • Waitress: 2.1.2 (pip, servidor WSGI puro Python, apto para stream chunked)
  • Red y acceso
  • Wi‑Fi configurado (2.4 GHz), IP accesible desde tu PC.
  • Acceso SSH habilitado o terminal local con teclado/monitor.
  • Conocimientos previos
  • Linux y shell en Raspberry Pi.
  • Python 3.11, virtualenvs, pip.
  • Conceptos básicos de HTTP y streaming MJPEG.

Comando para verificar versiones (ejecútalos y anota tu entorno real):

python3 --version
pip3 --version
gcc --version | head -n1
cmake --version | head -n1
apt-cache policy libcamera0 libcamera-apps python3-picamera2 | sed -n '1,6p'

Materiales

  • Raspberry Pi Zero W (modelo exacto).
  • Camera Module v2 (sensor Sony IMX219, con cable CSI de 15 pines).
  • Tarjeta microSD (≥ 16 GB, clase A1/A2 preferible).
  • Fuente de alimentación: 5 V / 2 A micro-USB.
  • Cable plano CSI adecuado para Pi Zero W (el conector es de tamaño “corto”, suele venir con la cámara; si no, compra el cable específico para Zero).
  • Adaptador micro-USB OTG (opcional, para teclado/USB si no usas SSH).
  • PC para flashear la imagen de Raspberry Pi OS Bookworm Lite (32-bit).

Nota: Este proyecto no requiere hardware adicional de E/S GPIO; todo el flujo es CSI → ISP/libcamera → servidor HTTP.

Preparación y conexión

Preparación del sistema

  1. Flashea Raspberry Pi OS Bookworm Lite (32-bit) en la microSD con Raspberry Pi Imager.
  2. Selecciona: Raspberry Pi OS Lite (32-bit) — Bookworm.
  3. Configura en Imager (opciones avanzadas): hostname, usuario, contraseña, Wi‑Fi y SSH.
  4. Inserta la microSD en la Raspberry Pi Zero W.
  5. Conecta la alimentación y espera el primer arranque.

Conexión de la cámara CSI

  • Apaga completamente la Raspberry Pi antes de manipular la cámara.
  • Localiza el conector CSI de la Raspberry Pi Zero W (interfaz de cámara).
  • Inserta el cable plano:
  • Contactos del cable hacia los contactos del conector CSI.
  • Bloquea la pestaña con cuidado para que no se suelte.
  • Conecta el otro extremo al Camera Module v2 respetando la orientación (contactos a contactos).
  • Enciende la Raspberry Pi.

Tabla de puertos y orientación (Pi Zero W + Camera Module v2)

Elemento Conector/Ubicación Detalle/Orientación
CSI (cámara) ZIF 15 pines (lado cámara) Alinear contactos del cable con los del ZIF; cerrar pestaña de bloqueo
Alimentación micro-USB (PWR IN) 5 V / 2 A recomendado
USB OTG (periféricos) micro-USB (USB) Para teclado/mouse/USB (opcional)
Tarjeta microSD Ranura microSD Insertar con la cara de contactos hacia la placa
LED estado PCB Útil para ver actividad del sistema

Habilitar interfaces y cámara (Bookworm)

En Bookworm con libcamera, normalmente no es necesario habilitar “Legacy Camera”. Aun así, verifica:

  • Opción A: raspi-config
  • sudo raspi-config
  • Interface Options:
    • I2C: Enable (útil para periféricos; no estrictamente necesario para libcamera, pero conveniente).
    • Legacy Camera: Disabled (para mantener libcamera).
  • Localisation Options: ajusta zona horaria y teclado si lo necesitas.
  • Finish y reboot.

  • Opción B: edición de /boot/firmware/config.txt

  • sudo nano /boot/firmware/config.txt
  • Asegúrate de tener (o añade) las líneas:
    camera_auto_detect=1
    # Forzar overlay del sensor si fuese necesario:
    # dtoverlay=imx219
  • Guarda y reinicia: sudo reboot

Comprobación rápida de la cámara con libcamera:

libcamera-hello -t 2000

Deberías ver en consola que se abre la cámara (en Pi Zero W sin pantalla HDMI, la aplicación igualmente informa si detecta el sensor). Si falla, revisa Troubleshooting.

Código completo

Implementaremos un servidor web Python que:
– Inicializa Picamera2 con una configuración “video-like” a baja resolución para la CPU del Pi Zero W.
– Captura frames periódicamente como arrays RGB.
– Codifica cada frame a JPEG con Pillow (calidad ajustable).
– Expone:
– /stream.mjpg → MJPEG multipart (para navegadores y VLC).
– /snapshot.jpg → fotograma actual (single shot).
– /health → estado simple.

Notas de diseño:
– Elegimos 640×480 a ~8–10 fps para un equilibrio entre CPU/latencia/calidad en el Zero W.
– Creamos un hilo productor que inserta frames JPEG en una cola bounded (drop-old) para evitar backlog.
– Usamos Flask por sencillez y Waitress como servidor WSGI robusto en producción ligera (ambos Python puro).

Estructura del proyecto

  • ~/mjpeg-csi/
  • app.py
  • .env (opcional, variables de configuración)
  • venv/ (virtualenv con paquetes pip)
  • run.sh (opcional, arranque)
  • servicio systemd (descrito más adelante)

app.py

#!/usr/bin/env python3
import io
import os
import signal
import threading
import time
from queue import Queue, Full, Empty
from typing import Generator, Optional

from flask import Flask, Response, jsonify, stream_with_context
from PIL import Image
from picamera2 import Picamera2

# Configuración desde entorno (con valores por defecto seguros para Pi Zero W)
WIDTH = int(os.getenv("MJPEG_WIDTH", "640"))
HEIGHT = int(os.getenv("MJPEG_HEIGHT", "480"))
FPS = float(os.getenv("MJPEG_FPS", "8"))  # 8 fps razonable en Zero W
JPEG_QUALITY = int(os.getenv("MJPEG_QUALITY", "70"))  # 50-80 recomendado
BOUNDARY = os.getenv("MJPEG_BOUNDARY", "frameboundary")
PORT = int(os.getenv("MJPEG_PORT", "8080"))
HOST = os.getenv("MJPEG_HOST", "0.0.0.0")
QUEUE_SIZE = int(os.getenv("MJPEG_QUEUE_SIZE", "2"))
CAPTURE_SLEEP = float(os.getenv("MJPEG_CAPTURE_SLEEP", str(1.0 / FPS)))

app = Flask(__name__)

# Cola de frames JPEG
frame_queue: "Queue[bytes]" = Queue(maxsize=QUEUE_SIZE)
shutdown_event = threading.Event()


def camera_thread():
    """
    Hilo productor:
    - Inicializa Picamera2
    - Captura arrays RGB a la resolución dada
    - Codifica a JPEG con Pillow
    - Inserta frames a la cola con política drop-old
    """
    picam2 = Picamera2()

    # Configuración "video" con salida RGB888 para codificar fácilmente a JPEG
    video_config = picam2.create_video_configuration(
        main={"size": (WIDTH, HEIGHT), "format": "RGB888"},
        transform=None  # sin rotación
    )
    picam2.configure(video_config)

    # Ajustes de control (opcionales)
    # Nota: En Zero W, mantener autoexposición/awb suele ser suficiente
    # picam2.set_controls({"AwbEnable": True, "AeEnable": True})

    picam2.start()
    try:
        while not shutdown_event.is_set():
            # Captura frame como array RGB
            frame = picam2.capture_array("main")

            # Codifica a JPEG
            with io.BytesIO() as buf:
                # Pillow espera array en RGB ordenado (ya lo tenemos)
                Image.fromarray(frame).save(
                    buf, format="JPEG", quality=JPEG_QUALITY, optimize=True
                )
                jpg = buf.getvalue()

            # Inserta en la cola, descartando el más antiguo si está llena
            try:
                frame_queue.put(jpg, timeout=0.01)
            except Full:
                try:
                    frame_queue.get_nowait()
                except Empty:
                    pass
                # Reintenta tras descartar
                try:
                    frame_queue.put_nowait(jpg)
                except Full:
                    pass

            # Ritmo de captura
            if CAPTURE_SLEEP > 0:
                time.sleep(CAPTURE_SLEEP)
    finally:
        picam2.stop()


def mjpeg_generator() -> Generator[bytes, None, None]:
    """
    Generador de stream MJPEG en formato multipart/x-mixed-replace.
    Cada iteración produce:
    --BOUNDARY
    Content-Type: image/jpeg
    Content-Length: <n>

    <bytes JPEG>
    """
    boundary_bytes = BOUNDARY.encode("ascii")
    while not shutdown_event.is_set():
        try:
            jpg = frame_queue.get(timeout=1.0)
        except Empty:
            continue

        header = (
            b"--" + boundary_bytes + b"\r\n"
            b"Content-Type: image/jpeg\r\n"
            b"Content-Length: " + str(len(jpg)).encode("ascii") + b"\r\n\r\n"
        )
        yield header + jpg + b"\r\n"


@app.route("/stream.mjpg")
def stream():
    return Response(
        stream_with_context(mjpeg_generator()),
        mimetype=f"multipart/x-mixed-replace; boundary={BOUNDARY}",
    )


@app.route("/snapshot.jpg")
def snapshot():
    # Toma el último frame disponible (o espera un poco)
    try:
        jpg = frame_queue.get(timeout=2.0)
    except Empty:
        return Response(status=503)
    return Response(jpg, mimetype="image/jpeg")


@app.route("/health")
def health():
    return jsonify(
        status="ok",
        width=WIDTH,
        height=HEIGHT,
        fps=FPS,
        quality=JPEG_QUALITY,
        queue_size=QUEUE_SIZE,
        boundary=BOUNDARY,
    )


def handle_sigterm(signum, frame):
    shutdown_event.set()


def main():
    # Inicia el hilo de cámara
    t = threading.Thread(target=camera_thread, name="camera-thread", daemon=True)
    t.start()

    # Señales para apagado limpio
    signal.signal(signal.SIGTERM, handle_sigterm)
    signal.signal(signal.SIGINT, handle_sigterm)

    # Servidor WSGI
    use_waitress = os.getenv("USE_WAITRESS", "1") == "1"
    if use_waitress:
        try:
            from waitress import serve
            # threads controla peticiones concurrentes (streams simultáneos)
            serve(app, host=HOST, port=PORT, threads=4)
        except Exception as e:
            print(f"[waitress] Error: {e}; usando servidor Flask de desarrollo.")
            app.run(host=HOST, port=PORT, threaded=True)
    else:
        app.run(host=HOST, port=PORT, threaded=True)

    shutdown_event.set()
    t.join(timeout=2.0)


if __name__ == "__main__":
    main()

Puntos clave del código:
– create_video_configuration con formato RGB888 simplifica la compresión JPEG con Pillow. Para el Zero W, bajar resolución o FPS si ves CPU alta.
– Cola bounded con política drop-old para minimizar latencia en el stream.
– Endpoints claros y reusables en validación.
– Waitress para servir responses “chunked” eficientes y estables; en caso de fallo, usa el servidor de desarrollo de Flask.

Compilación/flash/ejecución

A continuación, pasos reproducibles, exactos y ordenados.

1) Actualización de sistema y paquetes base

sudo apt update
sudo apt full-upgrade -y
sudo reboot

2) Instalar dependencias del sistema

  • libcamera y herramientas, Picamera2, Python 3.11, venv y utilidades:
sudo apt install -y \
  libcamera-apps \
  python3-picamera2 \
  python3-pip \
  python3-venv \
  python3-numpy \
  git \
  gcc \
  cmake

Comprobación rápida de cámara:

libcamera-hello -t 2000

Si ves que abre la cámara sin error, continúa.

3) Preparar directorio de proyecto y virtualenv

mkdir -p ~/mjpeg-csi
cd ~/mjpeg-csi
python3 -m venv --system-site-packages venv
source venv/bin/activate

Usamos –system-site-packages para que el venv vea python3-picamera2 instalado por apt.

4) Configurar piwheels y pip

Piwheels acelera instalación en Raspberry Pi y ofrece ruedas para ARMv6 (Zero W).

pip install --upgrade pip==23.2.1
pip config set global.index-url https://www.piwheels.org/simple

5) Instalar dependencias Python (pinned)

pip install \
  Flask==2.3.3 \
  Pillow==10.3.0 \
  waitress==2.1.2

Comprueba versiones:

python -c "import flask, PIL; import waitress; \
print('Flask', flask.__version__); \
print('Pillow', PIL.__version__); \
import pkgutil; import waitress; print('Waitress', waitress.__version__)"

6) Crear el archivo app.py

Copia el contenido de la sección anterior en:

nano app.py
# pega el código, guarda con Ctrl+O y sal con Ctrl+X

Dale permisos de ejecución:

chmod +x app.py

7) Ejecutar en modo desarrollo (prueba rápida)

source venv/bin/activate
export USE_WAITRESS=0
python app.py

Abre desde tu PC:
– Stream MJPEG: http://:8080/stream.mjpg
– Snapshot: http://:8080/snapshot.jpg
– Salud: http://:8080/health

Para detener: Ctrl+C.

8) Ejecutar con Waitress (recomendado para uso continuado)

source venv/bin/activate
export USE_WAITRESS=1
python app.py

Si todo funciona, automatiza con systemd.

9) Systemd service para autoarranque

Crea el servicio:

sudo tee /etc/systemd/system/mjpeg-csi.service >/dev/null <<'EOF'
[Unit]
Description=Servidor MJPEG (Picamera2) en Raspberry Pi Zero W
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/mjpeg-csi
Environment="USE_WAITRESS=1"
Environment="MJPEG_WIDTH=640"
Environment="MJPEG_HEIGHT=480"
Environment="MJPEG_FPS=8"
Environment="MJPEG_QUALITY=70"
Environment="MJPEG_PORT=8080"
Environment="MJPEG_HOST=0.0.0.0"
Environment="MJPEG_QUEUE_SIZE=2"
Environment="MJPEG_BOUNDARY=frameboundary"
Environment="MJPEG_CAPTURE_SLEEP=0.125"
ExecStart=/home/pi/mjpeg-csi/venv/bin/python /home/pi/mjpeg-csi/app.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

Recarga, habilita e inicia:

sudo systemctl daemon-reload
sudo systemctl enable --now mjpeg-csi.service
systemctl status mjpeg-csi.service --no-pager

Valida en el navegador: http://:8080/stream.mjpg

Validación paso a paso

  1. Comprobación de hardware CSI:
  2. Cámara correctamente conectada (cable firme, pestañas ZIF cerradas).
  3. Reinicio tras conexión.

  4. Comprobación libcamera:

  5. libcamera-hello -t 2000 no debe mostrar “No cameras available”.
  6. libcamera-hello --version muestra versión de libcamera-apps. Ejemplo:

    • libcamera-apps version 1.1.x (en Bookworm, varía según build de RPi OS).
  7. Comprobación de Picamera2 en Python:

  8. Inicia Python: python -c "from picamera2 import Picamera2; print('OK picamera2')".

  9. Arranque de servidor:

  10. systemctl status mjpeg-csi.service → “active (running)”.
  11. Logs iniciales en journalctl -u mjpeg-csi.service -n 50 --no-pager.

  12. Conectividad:

  13. curl -sI http://<IP>:8080/health → HTTP/1.1 200 OK y JSON con parámetros.
  14. curl -sI http://<IP>:8080/stream.mjpg → Content-Type: multipart/x-mixed-replace; boundary=frameboundary.
  15. curl -o /dev/null http://<IP>:8080/snapshot.jpg -v → Content-Type: image/jpeg.

  16. Visualización:

  17. Navegador (Chrome/Firefox): abrir http://:8080/stream.mjpg; deberías ver vídeo fluido (~8 fps).
  18. VLC: Medio → Abrir ubicación de red → http://:8080/stream.mjpg.
  19. ffplay (opcional en PC): ffplay -fflags nobuffer -flags low_delay -framedrop http://<IP>:8080/stream.mjpg.

  20. Rendimiento en el Zero W:

  21. top o htop: CPU del proceso Python entre 60–95% según calidad/fps/resolución.
  22. Ajusta variables (anchura, altura, FPS, calidad) si observas throttling o cortes.

  23. Persistencia:

  24. Reinicia el Pi: sudo reboot.
  25. Valida que el servicio levanta automáticamente y el stream responde.

Troubleshooting

1) Error: “No cameras available” con libcamera-hello
– Causas:
– Cable CSI mal orientado o suelto.
– Cámara defectuosa.
– dt/config no detecta el sensor.
– Soluciones:
– Apaga, reconecta cable CSI en ambos extremos; verifica orientación de contactos.
– En /boot/firmware/config.txt añade: camera_auto_detect=1 y, si persiste, fuerza dtoverlay=imx219.
– Actualiza firmware: sudo apt update && sudo apt full-upgrade -y && sudo reboot.

2) ImportError: No module named ‘picamera2’
– Causas: Falta el paquete apt o no se ve desde el venv.
– Solución:
sudo apt install -y python3-picamera2
– Crea el venv con --system-site-packages y actívalo de nuevo.
– Comprueba en Python: from picamera2 import Picamera2.

3) “Address already in use” al arrancar el servidor
– Causa: Puerto 8080 ocupado.
– Solución:
– Cambia MJPEG_PORT (p. ej., 8081) en el servicio systemd y systemctl daemon-reload && systemctl restart mjpeg-csi.service.
– Verifica puertos: ss -ltnp | grep :8080.

4) Latencia alta o stream entrecortado
– Causas: CPU saturada (Zero W es limitado).
– Ajustes:
– Reduce resolución: MJPEG_WIDTH=426, MJPEG_HEIGHT=240.
– Baja FPS: MJPEG_FPS=6 (y MJPEG_CAPTURE_SLEEP acorde ~0.166).
– Baja calidad JPEG: MJPEG_QUALITY=60.
– Verifica Wi‑Fi (RSSI) y evita congestión 2.4 GHz.

5) Imagen muy oscura/borrosa
– Causas: Iluminación insuficiente, exposiciones largas.
– Soluciones:
– Mejora iluminación.
– Baja FPS (para permitir mayor exposición) o considera fijar controles en Picamera2 (AeEnable=True, ISO).
– Limpia el protector/óptica.

6) Waitress no responde al stream tras varios clientes
– Causas: Límite de threads insuficiente en Waitress vs. clientes simultáneos.
– Solución:
– Incrementa threads en el servicio (p. ej., 6–8).
– Recuerda que más clientes aumentan la carga en CPU.

7) El stream no abre en Safari/iOS
– Causas: Implementación MJPEG y/o caché.
– Soluciones:
– Asegura cabecera correcta multipart con boundary constante (ya lo hace el código).
– Prueba otra app (VLC) o un navegador alternativo; Safari a veces interrumpe MJPEG en ciertas condiciones de red.

8) “Broken pipe” o desconexiones frecuentes
– Causas: Cortes Wi‑Fi, clientes que cierran y servidor aún enviando frames.
– Soluciones:
– Estas excepciones suelen ser benignas; observa logs y mantén la red estable.
– Reduce FPS/Quality para ahorrar ancho de banda.

Mejoras/variantes

  • Encoder alternativo con MJPEGEncoder de Picamera2:
  • Implementar un Output personalizado que reciba frames JPEG directos del encoder, reduciendo CPU. Requiere profundizar en la API de encoders/outputs de Picamera2.
  • Doble endpoint con distintas calidades:
  • /stream_small.mjpg (426×240 @ 8 fps, Q=60)
  • /stream_large.mjpg (640×480 @ 8 fps, Q=75) si la CPU lo permite.
  • Autenticación básica:
  • Añade un decorador simple o usa un reverse proxy Nginx con auth.
  • HTTPS:
  • Coloca Nginx delante (TLS) y haz proxy_pass a 127.0.0.1:8080.
  • Control de cámara:
  • Endpoints para cambiar parámetros en vivo (FPS, calidad), guardándolos en un archivo .env.
  • Grabación puntual:
  • Endpoint /record?n=10 para capturar N segundos a JPEGs o MKV con ffmpeg (ojo con carga del Zero W).
  • Métricas:
  • Exponer /metrics con tiempos de frame, colas, uso de CPU (psutil).

Checklist de verificación

  • [ ] Hardware:
  • [ ] Raspberry Pi Zero W con fuente 5 V/2 A estable.
  • [ ] Camera Module v2 con cable CSI bien orientado y pestillos cerrados.
  • [ ] microSD con Raspberry Pi OS Bookworm Lite (32-bit).
  • [ ] Sistema:
  • [ ] sudo apt update && sudo apt full-upgrade -y ejecutado sin errores.
  • [ ] libcamera-hello -t 2000 detecta la cámara.
  • [ ] Toolchain / Paquetes:
  • [ ] python3-picamera2 instalado por apt.
  • [ ] venv creado con –system-site-packages.
  • [ ] pip configurado con piwheels.
  • [ ] Flask==2.3.3, Pillow==10.3.0, Waitress==2.1.2 instalados.
  • [ ] Código:
  • [ ] app.py creado y ejecutable.
  • [ ] Variables (MJPEG_WIDTH/HEIGHT/FPS/QUALITY) ajustadas a tu entorno.
  • [ ] Ejecución:
  • [ ] python app.py entrega /health con status ok.
  • [ ] /stream.mjpg visible en navegador o VLC.
  • [ ] /snapshot.jpg devuelve un JPEG válido.
  • [ ] Servicio:
  • [ ] systemd mjpeg-csi.service en estado “active (running)”.
  • [ ] Arranca al boot y responde tras reinicio.
  • [ ] Rendimiento:
  • [ ] CPU en rango aceptable; si no, reduce resolución/FPS/calidad.
  • [ ] Sin cortes frecuentes; Wi‑Fi estable.

Con este caso práctico, dispones de un servidor web MJPEG eficiente y reproducible con el conjunto “Raspberry Pi Zero W + Camera Module v2” usando la pila libcamera y Python 3.11 en Raspberry Pi OS Bookworm (32-bit). Si en el futuro migras a placas más potentes (por ejemplo, Pi 3/4/5), puedes incrementar resolución, FPS y calidad, o sustituir la compresión por encoders más exigentes sin alterar la arquitectura básica del servidor.

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 recomendado para el Raspberry Pi Zero W?




Pregunta 2: ¿Qué versión de Python se recomienda utilizar?




Pregunta 3: ¿Cuál es la versión mínima de GCC recomendada?




Pregunta 4: ¿Qué comando se utiliza para verificar la versión de libcamera?




Pregunta 5: ¿Qué tipo de red se debe configurar para el Raspberry Pi Zero W?




Pregunta 6: ¿Qué herramienta se recomienda para el servidor WSGI puro Python?




Pregunta 7: ¿Cuál es la versión de Flask que se debe utilizar?




Pregunta 8: ¿Qué módulo de Python se recomienda para la cámara?




Pregunta 9: ¿Qué tipo de streaming se menciona en el artículo?




Pregunta 10: ¿Cuál es la imagen recomendada para el Raspberry Pi Zero W?




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: