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: