Practical case: Agri LoRaWAN MKR WAN 1310+BME680+DS18B20

Practical case: Agri LoRaWAN MKR WAN 1310+BME680+DS18B20 — hero

Objective and use case

What you’ll build: A LoRaWAN-enabled microclimate node for agriculture using the Arduino MKR WAN 1310, BME680, and DS18B20 to measure and transmit environmental data.

Why it matters / Use cases

  • Monitor air quality and temperature in greenhouses to optimize plant growth.
  • Track soil moisture levels in remote fields to improve irrigation efficiency.
  • Provide farmers with real-time data on microclimate conditions to make informed decisions.
  • Utilize low-power LoRaWAN communication for long-range data transmission in rural areas.

Expected outcome

  • Achieve reliable LoRaWAN OTAA join with periodic uplinks every 15 minutes.
  • Measure air temperature with a precision of ±0.5°C and humidity with ±3% accuracy.
  • Transmit data payloads of 11 bytes efficiently over LoRaWAN.
  • Maintain a low-power duty cycle to extend battery life beyond 1 year.

Audience: Agricultural engineers; Level: Advanced

Architecture/flow: Arduino MKR WAN 1310 collects data from BME680 and DS18B20, encodes it, and sends it via LoRaWAN to The Things Stack.

Advanced Hands‑On: LoRa Agro Microclimate Node with Arduino MKR WAN 1310 + BME680 + DS18B20 (lora-agro-microclima-node)

This practical case guides you through building an advanced LoRaWAN-enabled microclimate node tailored for agricultural monitoring, using the exact device model: Arduino MKR WAN 1310 + BME680 + DS18B20. You will deploy a battery-friendly node that measures air temperature/humidity/pressure/gas (VOC proxy) and soil/liquid temperature, encodes the data into a compact binary payload, sends it over LoRaWAN (OTAA) to The Things Stack (TTN v3), and supports configurable sampling intervals via downlink.

Key goals:
– Reliable LoRaWAN OTAA join and periodic uplinks.
– Accurate sensor sampling with proper oversampling and stabilization for BME680.
– DS18B20 on 1‑Wire with a 4.7 kΩ pull‑up resistor.
– Compact binary payload (11 bytes) with a documented uplink format and a TTN payload formatter.
– Low‑power duty cycle and watchdog considerations for field reliability.

Important note on toolchain: family defaults to Arduino UNO + Arduino CLI; however, this project uses a different board (Arduino MKR WAN 1310). Therefore we use PlatformIO (CLI) for build/flash/monitor commands. No GUI is required.


Prerequisites

  • Operating system:
  • Windows 10/11, macOS 12+ (Monterey or newer), or Ubuntu 22.04 LTS.
  • PlatformIO Core (CLI) installed via pip:
  • Python 3.10+ recommended.
  • Verified with PlatformIO Core 6.1.13.
  • The Things Stack (TTN v3) account:
  • Application created in the desired cluster.
  • Frequency plan matching your region (e.g., EU868, US915, AU915, AS923, KR920, IN865).
  • A device registered with OTAA:
    • Device EUI (generated by TTN or read from the MKR).
    • App EUI (Join EUI).
    • App Key (16 bytes).
  • USB cable (USB‑A to Micro‑B) for Arduino MKR WAN 1310.
  • Antenna attached to the MKR WAN 1310’s u.FL connector (mandatory before any RF transmission).
  • Basic familiarity with:
  • LoRaWAN OTAA workflow.
  • 1‑Wire topology and pull‑up resistors.
  • I2C bus concepts for sensor addressing and pull‑ups.
  • Serial port permissions:
  • Linux: add your user to the dialout group and re‑login.
    • Command: sudo usermod -aG dialout $USER

Driver notes:
– Arduino MKR WAN 1310 is a native USB CDC device; on Windows 10/11 and macOS, no third‑party drivers (CP210x/CH34x) are required. It appears as “Arduino MKR WAN 1310 (COMx)” or a tty device on Unix-like systems.


Materials (Exact Models)

  • Arduino MKR WAN 1310 (ABX00029) with LoRa antenna (included in kit).
  • Environmental sensor: Bosch BME680 breakout (I2C). Example: Adafruit BME680 (Product ID 3660) or equivalent, configured for I2C address 0x76 or 0x77.
  • 1‑Wire temperature sensor: DS18B20 (waterproof probe version is ideal for soil/liquid), TO‑92 or encapsulated probe.
  • 4.7 kΩ resistor (±5% or better) for 1‑Wire data line pull‑up.
  • Jumper wires (male/female as needed).
  • Power:
  • USB power during development.
  • Optional: 3.7 V LiPo cell for field deployment (connect to MKR BAT connector).
  • Optional environmental protection:
  • Enclosure with breathable membrane for BME680 (to avoid condensation and allow gas diffusion).
  • Cable glands and waterproofing for DS18B20 probe and enclosure.

Setup/Connection

The MKR WAN 1310 is a 3.3 V logic board. Do not connect 5 V signals to its pins.

  1. Antenna
  2. Carefully attach the u.FL antenna to the MKR WAN 1310 connector before powering. Never transmit without an antenna.

  3. BME680 (I2C)

  4. Power: VCC → 3.3 V; GND → GND.
  5. I2C: SDA → MKR pin marked SDA; SCL → MKR pin marked SCL.
  6. Address:

    • Most boards default to 0x76; some to 0x77. Check your breakout’s solder jumper.
    • We will default to 0x76 in code and log a warning if detection fails.
  7. DS18B20 (1‑Wire)

  8. If using a waterproof 3‑wire probe (typical color code):
    • Red → 3.3 V
    • Black → GND
    • Yellow/White → Data
  9. Use a 4.7 kΩ resistor between Data and 3.3 V at the MKR side.
  10. Data pin on MKR: D4 (configurable in code).
  11. Do not power DS18B20 from 5 V. Use 3.3 V to match logic level.

  12. Power / USB

  13. Connect the MKR to your PC via USB.
  14. For field operation, attach a LiPo to BAT (observe polarity) and keep the antenna connected.

Table summary of connections:

Module/Signal MKR WAN 1310 Pin Notes
Antenna u.FL RF connector Mandatory before RF TX
BME680 VCC 3.3 V Power 3.3 V only
BME680 GND GND Common ground
BME680 SDA SDA I2C data
BME680 SCL SCL I2C clock
DS18B20 VCC 3.3 V Power 3.3 V only
DS18B20 GND GND Common ground
DS18B20 DATA D4 1‑Wire data line
1‑Wire Pull‑up D4 ↔ 3.3 V (via 4.7 kΩ) Required pull‑up resistor

Full Code

We’ll use PlatformIO with the Arduino framework and the official MKRWAN library. The payload is a compact custom binary:
– Byte 0: Protocol version (0x01)
– Bytes 1–2: DS18B20 temperature (centi‑C, signed int16)
– Bytes 3–4: BME680 temperature (centi‑C, signed int16)
– Bytes 5–6: BME680 humidity (centi‑%, unsigned uint16)
– Bytes 7–8: BME680 pressure (deci‑hPa, unsigned uint16)
– Bytes 9–10: BME680 gas resistance (kΩ, unsigned uint16; clamped)

Create platformio.ini and src/main.cpp as shown.

platformio.ini:

; lora-agro-microclima-node/platformio.ini
[env:mkrwan1310]
platform = atmelsam
board = mkrwan1310
framework = arduino
monitor_speed = 115200
build_flags =
  -DLOG_LEVEL=1
lib_deps =
  arduino-libraries/MKRWAN@^1.1.7
  adafruit/Adafruit BME680 Library@^2.0.3
  adafruit/Adafruit BusIO@^1.14.5
  paulstoffregen/OneWire@^2.3.7
  milesburton/DallasTemperature@^3.11.0
  arduino-libraries/ArduinoLowPower@^1.2.2

src/main.cpp:

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

// ---------------------- Configuration ----------------------

// Select LoRaWAN region: EU868, US915, AS923, AU915, KR920, IN865
// Example: EU868
#define LORA_REGION EU868

// Replace with your real credentials from The Things Stack (TTN v3)
String appEui = "70B3D57ED0000000";  // JoinEUI (16 hex, no spaces)
String appKey = "00112233445566778899AABBCCDDEEFF";  // AppKey (32 hex)

// FPort to use for uplinks (and to listen for downlinks)
static const uint8_t LORA_FPORT = 10;

// Confirmed uplink every N transmissions (set 0 to disable)
static const uint8_t CONFIRM_EVERY = 4;

// Initial uplink period (seconds); can be reconfigured by downlink (FPort 10)
static uint32_t uplinkPeriodSec = 300;

// DS18B20 1-Wire pin
static const uint8_t ONEWIRE_PIN = 4;

// BME680 I2C address; try 0x76, fallback to 0x77
static const uint8_t BME680_ADDR_1 = 0x76;
static const uint8_t BME680_ADDR_2 = 0x77;

// ---------------------- Globals ----------------------

LoRaModem modem;
Adafruit_BME680 bme;
OneWire oneWire(ONEWIRE_PIN);
DallasTemperature ds18(&oneWire);

bool bmePresent = false;
bool dsPresent = false;
uint32_t frameCounter = 0;

// ---------------------- Utilities ----------------------

static void logf(const char* fmt, ...) {
#if LOG_LEVEL
  static char buf[256];
  va_list ap;
  va_start(ap, fmt);
  vsnprintf(buf, sizeof(buf), fmt, ap);
  va_end(ap);
  Serial.println(buf);
#endif
}

static int16_t toCentiC(float c) {
  if (isnan(c)) return INT16_MIN;
  long v = lroundf(c * 100.0f);
  if (v > INT16_MAX) v = INT16_MAX;
  if (v < INT16_MIN) v = INT16_MIN;
  return (int16_t)v;
}

static uint16_t toCentiPct(float rh) {
  if (isnan(rh)) return 0;
  long v = lroundf(rh * 100.0f);
  if (v < 0) v = 0;
  if (v > 10000) v = 10000;
  return (uint16_t)v;
}

static uint16_t toDeciHpa(float hpa) {
  if (isnan(hpa)) return 0;
  long v = lroundf(hpa * 10.0f);
  if (v < 0) v = 0;
  if (v > 65535) v = 65535;
  return (uint16_t)v;
}

static uint16_t toKiloOhm(float ohm) {
  if (isnan(ohm) || ohm < 0) return 0;
  long v = lroundf(ohm / 1000.0f);
  if (v > 65535) v = 65535;
  return (uint16_t)v;
}

// Downlink command: payload on FPort 10
// If payload = 0xA0 <uint16_t seconds>, set uplinkPeriodSec
static void handleDownlink(uint8_t* buf, int len) {
  if (len < 3) return;
  if (buf[0] == 0xA0) {
    uint16_t sec = (uint16_t)buf[1] << 8 | buf[2];
    if (sec >= 30 && sec <= 86400) {
      uplinkPeriodSec = sec;
      logf("[DN] Set uplink period to %us", uplinkPeriodSec);
    } else {
      logf("[DN] Ignored invalid period %u", sec);
    }
  }
}

// ---------------------- Setup ----------------------

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 5000) {
    ; // wait for serial if connected
  }
  logf("lora-agro-microclima-node starting...");
  logf("Board: Arduino MKR WAN 1310 | Region: %d", (int)LORA_REGION);

  // Ensure RF module is ready
  if (!modem.begin(LORA_REGION)) {
    logf("Failed to start LoRa modem. Check region and antenna.");
    while (1) { delay(1000); }
  }
  logf("Modem: %s", modem.version().c_str());
  logf("DevEUI (modem): %s", modem.deviceEUI().c_str());

  // Configure ADR and port
  modem.setADR(true);
  modem.setPort(LORA_FPORT);

  // OTAA join
  logf("Joining (OTAA)...");
  int connected = modem.joinOTAA(appEui, appKey);
  if (!connected) {
    logf("Join failed. Will retry every 30s.");
    for (;;) {
      delay(30000);
      if (modem.joinOTAA(appEui, appKey)) break;
      logf("Join retry failed...");
    }
  }
  logf("Joined network.");

  // I2C init
  Wire.begin();

  // BME680 detection and configuration
  if (bme.begin(BME680_ADDR_1)) {
    bmePresent = true;
    logf("BME680 detected at 0x%02X", BME680_ADDR_1);
  } else if (bme.begin(BME680_ADDR_2)) {
    bmePresent = true;
    logf("BME680 detected at 0x%02X", BME680_ADDR_2);
  } else {
    logf("BME680 not detected at 0x76/0x77. Check wiring/address.");
  }

  if (bmePresent) {
    bme.setTemperatureOversampling(BME680_OS_8X);
    bme.setHumidityOversampling(BME680_OS_2X);
    bme.setPressureOversampling(BME680_OS_4X);
    bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
    bme.setGasHeater(320, 150);
  }

  // DS18B20 init
  ds18.begin();
  dsPresent = (ds18.getDeviceCount() > 0);
  logf("DS18B20 devices: %d", ds18.getDeviceCount());
  if (!dsPresent) {
    logf("Warning: No DS18B20 detected on D4. Check 4.7k pull-up and wiring.");
  }

  // Put modem to sleep between TX; we’ll wake explicitly.
  modem.sleep();
}

// ---------------------- Main Loop ----------------------

void loop() {
  float ds_temp_c = NAN;
  float bme_temp_c = NAN, bme_rh = NAN, bme_hpa = NAN, bme_gas = NAN;

  // Read DS18B20
  if (dsPresent) {
    ds18.requestTemperatures();
    ds_temp_c = ds18.getTempCByIndex(0);
  }

  // Read BME680 (blocking performReading handles gas heater timing)
  if (bmePresent) {
    if (bme.performReading()) {
      bme_temp_c = bme.temperature;
      bme_rh = bme.humidity;
      bme_hpa = bme.pressure / 100.0f;
      bme_gas = bme.gas_resistance; // Ohms
    } else {
      logf("BME680 reading failed");
    }
  }

  // Build payload
  uint8_t payload[16];
  size_t len = 0;
  payload[len++] = 0x01; // protocol version

  int16_t ds_cC = toCentiC(ds_temp_c);
  int16_t bme_cC = toCentiC(bme_temp_c);
  uint16_t rh_cP = toCentiPct(bme_rh);
  uint16_t p_dhPa = toDeciHpa(bme_hpa);
  uint16_t gas_kohm = toKiloOhm(bme_gas);

  // Pack big-endian
  payload[len++] = (uint8_t)(ds_cC >> 8);
  payload[len++] = (uint8_t)(ds_cC & 0xFF);
  payload[len++] = (uint8_t)(bme_cC >> 8);
  payload[len++] = (uint8_t)(bme_cC & 0xFF);
  payload[len++] = (uint8_t)(rh_cP >> 8);
  payload[len++] = (uint8_t)(rh_cP & 0xFF);
  payload[len++] = (uint8_t)(p_dhPa >> 8);
  payload[len++] = (uint8_t)(p_dhPa & 0xFF);
  payload[len++] = (uint8_t)(gas_kohm >> 8);
  payload[len++] = (uint8_t)(gas_kohm & 0xFF);

  // Log
  logf("Frame %lu | DS18: %.2f C | BME: %.2f C, %.2f %%RH, %.2f hPa, %.0f ohm | period=%us",
       (unsigned long)frameCounter,
       ds_temp_c, bme_temp_c, bme_rh, bme_hpa, bme_gas, uplinkPeriodSec);

  // Wake modem and send
  modem.wake();

  bool confirmed = (CONFIRM_EVERY != 0) && ((frameCounter % CONFIRM_EVERY) == 0);

  int err = modem.beginPacket();
  if (err == 0) {
    modem.write(payload, len);
    int res = modem.endPacket(confirmed);
    if (res > 0) {
      logf("Uplink sent (%s). Bytes=%u",
           confirmed ? "confirmed" : "unconfirmed", (unsigned)len);
    } else {
      logf("Uplink failed (res=%d).", res);
    }
  } else {
    logf("beginPacket error: %d", err);
  }

  // Check for downlink (after a confirmed uplink there may be RX windows)
  int packetSize = modem.parsePacket();
  if (packetSize > 0) {
    uint8_t dn[64];
    int i = 0;
    while (modem.available() && i < (int)sizeof(dn)) {
      dn[i++] = modem.read();
    }
    logf("Downlink: port=%d bytes=%d", modem.getDownlinkPort(), i);
    handleDownlink(dn, i);
  }

  // Sleep modem and MCU
  modem.sleep();
  frameCounter++;

  // Low power sleep (approx)
  LowPower.sleep(uplinkPeriodSec * 1000UL);
}

Build/Flash/Run Commands (PlatformIO CLI)

  • Verify PlatformIO Core version:
pio --version
  • Create project structure (if starting from scratch):
mkdir -p lora-agro-microclima-node
cd lora-agro-microclima-node
pio project init --board mkrwan1310 --project-option "framework=arduino"
  • Place the provided platformio.ini at project root and main.cpp in src/.

  • Build:

pio run
  • Put the MKR WAN 1310 in a normal USB connected state. If needed, double-tap the reset button to enter bootloader mode (port may change to a “bootloader” COM/tty).

  • Upload:

pio run -t upload
  • Serial monitor (adjust port if needed):
pio device list
pio device monitor --baud 115200
  • If you need to set a specific port:
pio run -t upload --upload-port COM7     # Windows example
pio run -t upload --upload-port /dev/ttyACM0  # Linux example

Step‑by‑Step Validation

  1. Pre-flight checks
  2. Antenna securely attached to the MKR WAN 1310.
  3. BME680 wired to 3.3 V, GND, SDA, SCL; address known (0x76 or 0x77).
  4. DS18B20 wired to 3.3 V, GND, D4, with 4.7 kΩ pull‑up to 3.3 V on the D4 data line.
  5. TTN device registered (OTAA), frequency plan matches your region.

  6. Provisioning in The Things Stack (TTN v3)

  7. Create an application and register a device.
  8. Note the Device EUI, Join EUI (App EUI), and App Key.
  9. Paste JoinEUI and AppKey into main.cpp.
  10. For US915/AU915, ensure sub-band configuration on your gateway matches network plan (MKRWAN abstracts this, but gateway must match network).

  11. First boot and join

  12. Open serial monitor:
    • pio device monitor –baud 115200
  13. Expected logs:
    • Modem version and DevEUI
    • “Joining (OTAA)…” then “Joined network.”
    • Sensor presence logs (BME680 detected at 0x76/0x77, DS18B20 devices: N)
  14. If join fails, the code automatically retries every 30 seconds.

  15. Sensor validation

  16. Observe serial logs for numeric readings:
    • DS18: e.g., 21.56 C
    • BME: e.g., 22.14 C, 48.32 %RH, 1008.5 hPa, 12100 ohm
  17. Touch DS18B20 probe: temperature should rise within a few seconds.
  18. BME680 gas resistance responds slowly to VOC changes; do not expect immediate large swings. Humidity/temperature/pressure should look reasonable for your environment.

  19. Uplink validation on TTN

  20. In TTN Console, go to the device “Live data.”
  21. After join, you should see periodic uplinks on FPort 10 with 11‑byte payloads.
  22. Example payload (hex): 01 07 D0 08 04 13 88 27 23 00 96

    • Interpreted as:
    • ver=1
    • ds18=0x07D0=2000 => 20.00 C
    • bme_t=0x0804=2052 => 20.52 C
    • rh=0x1388=5000 => 50.00 %
    • p=0x2723=10019 => 1001.9 hPa
    • gas=0x0096=150 => 150 kΩ
  23. Payload formatter in TTN v3

  24. Application → Payload formatters → Uplink → Formatter type “Javascript.”
  25. Use this decoder to parse the custom binary:
function decodeUplink(input) {
  const bytes = input.bytes;
  if (!bytes || bytes.length < 11) {
    return { errors: ["invalid length"] };
  }
  const ver = bytes[0];
  const s16 = (hi, lo) => {
    let v = (hi << 8) | lo;
    if (v & 0x8000) v = v - 0x10000;
    return v;
  };
  const u16 = (hi, lo) => ((hi << 8) | lo) & 0xFFFF;

  const ds_cC = s16(bytes[1], bytes[2]);
  const bme_cC = s16(bytes[3], bytes[4]);
  const rh_cP = u16(bytes[5], bytes[6]);
  const p_dhPa = u16(bytes[7], bytes[8]);
  const gas_kohm = u16(bytes[9], bytes[10]);

  return {
    data: {
      version: ver,
      ds18_c: ds_cC / 100.0,
      bme_temp_c: bme_cC / 100.0,
      bme_rh_pct: rh_cP / 100.0,
      bme_hpa: p_dhPa / 10.0,
      bme_gas_kohm: gas_kohm
    },
    warnings: [],
    errors: []
  };
}
  • Save and return to Live data. You should now see decoded fields.

  • Downlink test to change sampling period

  • Construct a downlink with FPort 10 and payload format “Hex.”
  • Format: A0 00 78 sets period to 0x0078 = 120 seconds.
  • In TTN console: end device → Messaging → Downlink → FPort 10, Hex payload A00078 → Schedule downlink.
  • Watch serial logs for “[DN] Set uplink period to 120s.”
  • Confirm next uplinks occur every ~120 seconds.

  • Duty cycle and confirmed uplinks

  • The code uses confirmed uplinks every 4th frame by default (CONFIRM_EVERY=4).
  • Reduce confirmation frequency or disable (set 0) for production to conserve airtime and battery.

  • Low‑power behavior

  • Observe that the device sleeps between transmissions; current draw should drop significantly when running on battery (use a power meter if available).
  • Ensure BME680 heater usage is acceptable for your energy budget; our profile is moderate.

Troubleshooting

  • No COM/tty port appears:
  • Try a different USB cable/port.
  • Double‑tap reset to access bootloader; upload then try normal mode again.
  • Windows: Device Manager → Ports (COM & LPT) should show “Arduino MKR WAN 1310.”
  • Linux: adduser to dialout group and re‑login; check dmesg for ttyACM device.

  • Join fails repeatedly:

  • Verify AppEUI/JoinEUI and AppKey exact hex values, no spaces.
  • Check frequency plan and region constant (LORA_REGION) matches your TTN application’s plan and local regulations.
  • Gateway coverage and correct channel plan (especially for US915/AU915 sub‑bands).
  • Antenna firmly connected; never transmit without it.
  • If you re‑flashed many times and frame counters cause MIC failures, in TTN v3 device’s “Advanced MAC settings,” consider disabling frame counter checks for testing or re‑provision device session (then re‑enable for production).

  • Uplinks seen, but decoding fails:

  • Ensure the uplink formatter JS is installed at application or device level and set to Javascript.
  • Confirm payload length is 11 bytes (if length differs, you may have edited code; update decoder accordingly).

  • BME680 not detected:

  • Confirm wiring to SDA/SCL and 3.3 V, GND.
  • Check address: if your module is strapped to 0x77, either change the code or solder jumper.
  • Avoid long I2C runs; for field setups, keep wires short or use shielded cable.
  • Ensure pull‑ups on the breakout are present; most BME680 boards include them.

  • DS18B20 not detected:

  • Verify 4.7 kΩ pull‑up from D4 to 3.3 V is installed.
  • Confirm the sensor lead colors; some probes swap yellow/white. Identify with a multimeter or documentation.
  • Power with 3.3 V, not 5 V.
  • Parasite power mode is not used in this code; ensure 3‑wire mode.

  • Downlinks not received:

  • Only confirmed uplinks open RX windows that are more deterministic on some networks; schedule your downlink shortly after an uplink or use confirmed uplink events.
  • Check that the downlink FPort is 10 (matches code).
  • Keep downlink payload length small and timing near the next RX window (TTN handles scheduling).

  • Excessive gas resistance fluctuations:

  • Allow a burn‑in time for BME680 (several minutes) after power‑up.
  • Avoid enclosing the BME680 in airtight housings; use a vented enclosure with a hydrophobic membrane.

  • Build failures on libraries:

  • Ensure platformio.ini contains the exact lib_deps lines provided.
  • Run pio pkg update to refresh packages.

Improvements

  • Payload standardization:
  • Use CayenneLPP or a full SenML CBOR/JSON pipeline for broader interoperability. The custom binary is efficient but bespoke.

  • BSEC integration for IAQ:

  • Replace Adafruit BME680 library with Bosch Sensortec BSEC 2.x to compute IAQ, sIAQ, CO2eq, bVOC. Requires licensing terms and more flash/RAM.

  • Battery voltage measurement:

  • Implement VBAT read via internal divider if exposed on MKR WAN 1310 (consult board schematic). Expose in payload to monitor energy.

  • Event‑driven sampling:

  • Change interval based on diurnal profile or soil temperature dynamics. Implement hysteresis and backoff logic for network duty cycle.

  • Robustness:

  • Add a watchdog timer, brown‑out detection, and persistent storage (EEPROM emulation) for uplinkPeriodSec so downlink settings survive resets.

  • Security and compliance:

  • Enforce CFList/sub‑band settings as required by local regulations.
  • Avoid confirmed uplinks as default in production; use them sparingly for diagnostics.

  • Calibration:

  • Compare DS18B20 and BME680 temperature against a reference; apply per‑sensor offsets stored in flash.

  • Mechanical:

  • Thermally isolate BME680 from MCU heat sources; use a standoff and vented location.
  • Waterproof the DS18B20 cable ingress with proper glands and potting where needed.

Checklist

  • Antenna connected to MKR WAN 1310 u.FL.
  • PlatformIO Core installed and working (pio –version).
  • platformio.ini configured for mkrwan1310 with required lib_deps.
  • appEui and appKey entered correctly in main.cpp.
  • Region constant (LORA_REGION) matches TTN frequency plan.
  • BME680 wired to 3.3 V, GND, SDA/SCL; address known (0x76/0x77).
  • DS18B20 wired to 3.3 V, GND, D4 with 4.7 kΩ pull‑up to 3.3 V on D4.
  • Build succeeds (pio run).
  • Upload succeeds (pio run -t upload).
  • Serial monitor shows join success and sensible sensor readings.
  • TTN Console shows uplinks; payload formatter installed and decoding fields.
  • Optional: Downlink A0 00 78 received, sampling period updated to 120 s.
  • Confirmed uplink duty minimized; production interval aligned with regional duty cycle limits.

With these steps, you have a fully functional lora-agro-microclima-node on the Arduino MKR WAN 1310 using BME680 and DS18B20. It’s ready for field deployment, telemetry logging, and integration with dashboards or alerting pipelines.

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

Go to Amazon

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

Quick Quiz

Question 1: What device model is used for the LoRaWAN-enabled microclimate node?




Question 2: Which sensor is used for measuring air temperature, humidity, and pressure?




Question 3: What type of resistor is used with the DS18B20 sensor?




Question 4: What is the maximum payload size for uplink data in this project?




Question 5: Which operating systems are supported for this project?




Question 6: What is the recommended version of Python for installing PlatformIO Core?




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




Question 8: What type of downlink does the project support?




Question 9: Which toolchain is recommended for building and flashing the project?




Question 10: What is required to create a device registered with OTAA?




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

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

Follow me:


Practical case: Arduino Mega 2560 CAN logger MCP2515 & W5500

Practical case: Arduino Mega 2560 CAN logger MCP2515 & W5500 — hero

Objective and use case

What you’ll build: A predictive maintenance logger using Arduino Mega 2560, Seeed CAN-BUS Shield V2, and W5500 Ethernet Shield to monitor real-time data and log anomalies effectively.

Why it matters / Use cases

  • Monitor vehicle health by logging CAN data such as engine temperature and RPM to predict maintenance needs.
  • Track industrial machinery performance, capturing vibration and current trends to prevent unexpected failures.
  • Enable remote monitoring of equipment by sending UDP logs over Ethernet for real-time analysis.
  • Facilitate data-driven decision-making in fleet management through historical logging of CAN messages.

Expected outcome

  • Real-time logging of at least 100 CAN frames per second with minimal latency.
  • Accurate anomaly detection with a scoring system that identifies deviations from learned baselines.
  • Data stored on microSD with a retrieval time of under 1 second for analysis.
  • UDP stream transmission with a packet loss rate of less than 1% over a local network.

Audience: Intermediate to advanced Arduino users; Level: Advanced

Architecture/flow: Data ingestion from CAN bus → feature extraction → anomaly scoring → logging to microSD and UDP stream.

CAN Predictive Maintenance Logger on Arduino Mega 2560 + Seeed CAN-BUS Shield V2 + W5500 Ethernet Shield (Advanced)

This hands-on builds a predictive maintenance logger that ingests CAN frames, extracts condition-monitoring features in real time (e.g., vibration RMS, current and temperature trends), scores anomalies against learned baselines, and logs them to both a microSD card and a UDP stream over Ethernet. You’ll run it on:

  • Arduino Mega 2560
  • Seeed CAN-BUS Shield V2 (MCP2515 + TJA1050)
  • W5500 Ethernet Shield (Arduino Ethernet Shield 2–class hardware)

No circuit drawings are used—everything is described via text, tables, and code. We will use Arduino CLI (not the GUI) and the arduino:avr core, adapting the defaults to the chosen board: FQBN arduino:avr:mega.


Prerequisites

  • Comfortable with C/C++ for Arduino and basic embedded systems.
  • Familiarity with CAN bus basics and SPI bus sharing among multiple devices.
  • A network to receive UDP logs (e.g., a laptop on the same LAN).
  • Arduino CLI installed and available in PATH.

Optional for advanced validation:

  • A USB-to-CAN or SocketCAN-capable interface for a PC (e.g., CANable/CANtact, PEAK, ValueCAN) to generate real CAN frames.
  • Linux (or WSL) with can-utils for cansend/candump, if you want to inject test frames externally.

Materials (exact model)

  • Microcontroller: Arduino Mega 2560 (ATmega2560)
  • CAN: Seeed CAN-BUS Shield V2 (MCP2515 + TJA1050)
  • Ethernet: W5500 Ethernet Shield (Arduino Ethernet Shield 2–compatible)
  • microSD card (FAT32, 4–32 GB recommended) inserted into the Ethernet shield’s SD slot
  • USB cable for Mega 2560
  • CAN wiring to your target bus:
  • Twisted pair to CAN_H/CAN_L
  • Proper termination: 120 Ω at each end of the bus
  • Optional: USB‑CAN adapter for validation
  • Network switch/Router/Ethernet cable

Setup/Connection

Stacking order and SPI notes:

  • Stack the W5500 Ethernet Shield onto the Arduino Mega 2560 first.
  • Stack the Seeed CAN-BUS Shield V2 on top of the Ethernet Shield.
  • The shields share the SPI bus; each device must have a unique chip select (CS).

Pin and signal assignments on Arduino Mega 2560:

  • SPI is on the 6-pin ICSP header (used by both shields). On the Mega, SS is D53—set it as OUTPUT to stay in SPI master mode.
  • Ethernet W5500 CS: D10 (fixed on the Ethernet Shield).
  • Ethernet SD CS: D4 (fixed on the Ethernet Shield).
  • CAN MCP2515 CS: D9 (default on Seeed CAN-BUS Shield V2—leave the default solder pads as-is).
  • CAN INT: D2 (default on Seeed CAN-BUS Shield V2).

CAN bus wiring (DB9 on Seeed CAN-BUS Shield V2 using CiA standard):

  • Pin 7: CAN_H
  • Pin 2: CAN_L
  • Pin 3: GND
  • Termination: Use the on-board termination switch/resistor only if your node is at a bus end. Exactly two 120 Ω terminations in total on the bus.

Table: SPI/IO mapping summary

Function Shield Arduino Mega Pin(s) Notes
SPI SCK/MOSI/MISO Both shields via ICSP ICSP header Hardware SPI on Mega via ICSP
Ethernet W5500 CS W5500 Ethernet Shield D10 Must be OUTPUT and toggled by library
Ethernet SD CS W5500 Ethernet Shield (SD) D4 We’ll log to SD using this CS
CAN MCP2515 CS Seeed CAN-BUS Shield V2 D9 Default CS for V2
CAN INT Seeed CAN-BUS Shield V2 D2 Active LOW when frame pending
SPI Master SS D53 Set OUTPUT to keep Mega in master mode

Power:

  • Power the Mega 2560 via USB during development.
  • For field deployment, use a regulated 7–12 V DC barrel or a high-quality 5 V source to the 5 V pin with proper grounding.

Full Code (Arduino sketch)

Create a new sketch folder and file:
– Folder: ~/Arduino/can-predictive-maintenance-logger
– File: ~/Arduino/can-predictive-maintenance-logger/can-predictive-maintenance-logger.ino

Paste the following:

/*
  can-predictive-maintenance-logger
  Target: Arduino Mega 2560 + Seeed CAN-BUS Shield V2 (MCP2515+TJA1050) + W5500 Ethernet Shield

  Features:
  - Reads CAN frames via MCP2515 (CS=D9, INT=D2) at 500 kbps
  - Extracts predictive maintenance features over sliding windows
  - Baseline learning and anomaly scoring (z-score aggregate)
  - Logs JSONL over UDP and CSV to SD card (Ethernet shield SD CS=D4)
  - Optional internal CAN loopback mode with synthetic frame generator
*/

#include <SPI.h>
#include <mcp_can.h>          // Seeed Studio MCP2515
#include <Ethernet.h>         // W5500 Ethernet
#include <EthernetUdp.h>      // UDP transport
#include <SD.h>               // SD card (Ethernet shield slot)
#include <ArduinoJson.h>      // ArduinoJson v6

// ----------------- Hardware configuration -----------------
const uint8_t PIN_CS_CAN = 9;   // Seeed CAN-BUS Shield V2 CS
const uint8_t PIN_INT_CAN = 2;  // Seeed CAN-BUS Shield V2 INT
const uint8_t PIN_CS_ETH = 10;  // W5500 CS (fixed on shield)
const uint8_t PIN_CS_SD  = 4;   // SD CS (Ethernet shield)
const uint8_t PIN_SS_MEGA = 53; // Ensure Mega is SPI master

// ----------------- CAN setup -----------------
MCP_CAN CAN0(PIN_CS_CAN);
const uint32_t CAN_BAUD = CAN_500KBPS;   // typical for OBD-II/industrial
// Optional: change to CAN_250KBPS for other networks

// ----------------- Ethernet/UDP setup -----------------
byte mac[] = { 0xDE, 0xAD, 0xBE, 0x66, 0x25, 0x60 }; // unique MAC (avoid duplicates)
IPAddress staticIP(192, 168, 1, 200);                // fallback static IP
IPAddress staticDNS(192, 168, 1, 1);
IPAddress staticGW(192, 168, 1, 1);
IPAddress staticMask(255, 255, 255, 0);

// Remote collector (adjust as needed)
IPAddress collectorIP(192, 168, 1, 50);
const uint16_t collectorPort = 5000;

EthernetUDP udp;

// ----------------- SD logging -----------------
File logFile;
const char* csvName = "pm_log.csv";

// ----------------- Predictive Maintenance Model -----------------
// Assume the following CAN IDs carry process condition signals:
// 0x200: vibration_rms_mg (uint16, milli-g, little-endian)
// 0x201: motor_temp_dC (uint16, 0.1 C/LSB)
// 0x202: motor_current_dA (uint16, 0.1 A/LSB)
// 0x203: motor_rpm (uint16, RPM)

// Windowing
const size_t WINDOW_SIZE = 60;    // samples per window
const uint32_t SEND_INTERVAL_MS = 100; // expected sample interval per signal

struct Sample {
  float vib_rms;   // g
  float temp_c;    // C
  float current_a; // A
  float rpm;       // RPM
  uint32_t t_ms;   // timestamp (ms)
};

Sample win[WINDOW_SIZE];
size_t winCount = 0;

// Baseline (Welford online)
struct OnlineStats {
  double mean = 0.0;
  double m2 = 0.0;
  uint32_t n = 0;
  void update(double x) {
    n++;
    double d = x - mean;
    mean += d / n;
    double d2 = x - mean;
    m2 += d * d2;
  }
  double variance() const { return (n > 1) ? m2 / (n - 1) : 0.0; }
  double stddev() const { double v = variance(); return v > 0 ? sqrt(v) : 0.0; }
};

OnlineStats base_vib_mean;
OnlineStats base_vib_kurt;
OnlineStats base_current_rms;
OnlineStats base_temp_slope;
OnlineStats base_rpm_var;

// Mode: 1=internal loopback with synthetic frames, 0=normal receive
#define USE_LOOPBACK 1

// Synthetic generator
uint32_t lastGenMs = 0;
void generateSyntheticCAN() {
  if (millis() - lastGenMs < SEND_INTERVAL_MS) return;
  lastGenMs = millis();

  // Vibration: base 0.25 g +/- jitter, occasional anomalies
  static uint32_t tick = 0;
  tick++;
  float vib_g = 0.25 + 0.02 * sin(0.1 * tick) + 0.01 * ((tick % 10) - 5);
  if ((tick % 600) == 0) vib_g += 0.2; // burst anomaly every ~1 minute

  // Temp: slowly rising
  static float temp_c = 40.0;
  temp_c += 0.002; // ~0.12 C/min

  // Current: modest load, ripple
  float current_a = 2.0 + 0.3 * sin(0.05 * tick);

  // RPM: steady with noise
  float rpm = 1500 + 25 * sin(0.03 * tick);

  // Pack and send
  auto sendU16 = [](uint32_t id, uint16_t value) {
    byte buf[8] = { (byte)(value & 0xFF), (byte)(value >> 8), 0,0,0,0,0,0 };
    CAN0.sendMsgBuf(id, 0, 8, buf);
  };

  uint16_t vib_mg = (uint16_t)(vib_g * 1000.0f);
  uint16_t temp_dC = (uint16_t)(temp_c * 10.0f);
  uint16_t current_dA = (uint16_t)(current_a * 10.0f);
  uint16_t rpm_u16 = (uint16_t)(rpm);

  sendU16(0x200, vib_mg);
  sendU16(0x201, temp_dC);
  sendU16(0x202, current_dA);
  sendU16(0x203, rpm_u16);
}

// Helpers for feature calculations
double meanOf(const float* a, size_t n) {
  double s = 0; for (size_t i=0;i<n;i++) s += a[i]; return s / (double)n;
}
double varianceOf(const float* a, size_t n, double mean) {
  double s = 0; for (size_t i=0;i<n;i++){ double d=a[i]-mean; s+=d*d; } return s/(double)(n>1?(n-1):1);
}
double kurtosisExcess(const float* a, size_t n, double mean, double var) {
  if (n < 4 || var <= 0) return 0;
  double s4 = 0;
  for (size_t i=0;i<n;i++){
    double d=a[i]-mean; s4 += d*d*d*d;
  }
  double m2 = var * (n-1); // sample variance times (n-1)
  double k = (n * s4) / (m2 * m2) - 3.0;
  return k;
}
double rmsOf(const float* a, size_t n) {
  double s2=0; for (size_t i=0;i<n;i++){ s2 += (double)a[i]*a[i]; } return sqrt(s2/(double)n);
}
double slopeLinear(const float* y, const uint32_t* tms, size_t n) {
  // x in minutes to obtain slope per minute
  double sx=0, sy=0, sxy=0, sxx=0; size_t N=n;
  for (size_t i=0;i<n;i++){
    double x = tms[i] / 60000.0;
    sx += x; sy += y[i]; sxy += x*y[i]; sxx += x*x;
  }
  double denom = (N*sxx - sx*sx);
  if (denom == 0) return 0;
  return (N*sxy - sx*sy)/denom; // units of y per minute
}

// Storage for current window raw columns
float col_vib[WINDOW_SIZE], col_temp[WINDOW_SIZE], col_cur[WINDOW_SIZE], col_rpm[WINDOW_SIZE];
uint32_t col_t[WINDOW_SIZE];

// Parse a single CAN frame into the current sample (when available)
void handleCANFrame(unsigned long id, unsigned char len, unsigned char *buf) {
  uint32_t now = millis();
  static Sample current = {NAN,NAN,NAN,NAN,0};

  auto asU16 = [&](int idx)->uint16_t { return (uint16_t)(buf[idx] | (buf[idx+1]<<8)); };

  if (id == 0x200) current.vib_rms = (float)asU16(0) / 1000.0f;
  else if (id == 0x201) current.temp_c = (float)asU16(0) / 10.0f;
  else if (id == 0x202) current.current_a = (float)asU16(0) / 10.0f;
  else if (id == 0x203) current.rpm = (float)asU16(0);
  current.t_ms = now;

  // Once we have a complete set, push to the window
  if (!isnan(current.vib_rms) && !isnan(current.temp_c) && !isnan(current.current_a) && !isnan(current.rpm)) {
    if (winCount < WINDOW_SIZE) {
      win[winCount] = current;
      col_vib[winCount] = current.vib_rms;
      col_temp[winCount] = current.temp_c;
      col_cur[winCount] = current.current_a;
      col_rpm[winCount] = current.rpm;
      col_t[winCount] = current.t_ms;
      winCount++;
    }
    // reset for the next set
    current.vib_rms = current.temp_c = current.current_a = current.rpm = NAN;
  }
}

// Compute features, update baselines, produce outputs
void processWindow() {
  if (winCount < WINDOW_SIZE) return;

  // Basic features
  double vib_mean = meanOf(col_vib, winCount);
  double vib_var = varianceOf(col_vib, winCount, vib_mean);
  double vib_kurt = kurtosisExcess(col_vib, winCount, vib_mean, vib_var);
  double cur_rms = rmsOf(col_cur, winCount);
  double rpm_mean = meanOf(col_rpm, winCount);
  double rpm_var = varianceOf(col_rpm, winCount, rpm_mean);
  double temp_slope = slopeLinear(col_temp, col_t, winCount); // C per minute

  // Update baseline (first 10 windows learn baseline only)
  static uint32_t windowsSeen = 0;
  windowsSeen++;
  const bool learning = (windowsSeen <= 10);

  base_vib_mean.update(vib_mean);
  base_vib_kurt.update(vib_kurt);
  base_current_rms.update(cur_rms);
  base_temp_slope.update(temp_slope);
  base_rpm_var.update(rpm_var);

  // Anomaly scoring using z-score aggregate
  auto zscore = [](double x, const OnlineStats& s)->double {
    double sd = s.stddev();
    if (sd <= 1e-9) return 0;
    return (x - s.mean) / sd;
  };

  double z_vib_mean = zscore(vib_mean, base_vib_mean);
  double z_vib_kurt = zscore(vib_kurt, base_vib_kurt);
  double z_curr_rms = zscore(cur_rms, base_current_rms);
  double z_temp_slope = zscore(temp_slope, base_temp_slope);
  double z_rpm_var = zscore(rpm_var, base_rpm_var);

  double anomaly_score = z_vib_mean*z_vib_mean
                       + z_vib_kurt*z_vib_kurt
                       + z_curr_rms*z_curr_rms
                       + z_temp_slope*z_temp_slope
                       + z_rpm_var*z_rpm_var;

  // Emit JSON (UDP + Serial)
  StaticJsonDocument<320> doc;
  doc["uptime_ms"] = millis();
  doc["learning"] = learning;
  doc["win"] = (int)winCount;
  JsonObject f = doc.createNestedObject("feat");
  f["vib_mean_g"] = vib_mean;
  f["vib_kurt_ex"] = vib_kurt;
  f["curr_rms_A"] = cur_rms;
  f["temp_slope_C_per_min"] = temp_slope;
  f["rpm_var"] = rpm_var;
  doc["score"] = anomaly_score;

  char jsonBuf[380];
  size_t jsonLen = serializeJson(doc, jsonBuf, sizeof(jsonBuf));
  Serial.println(jsonBuf);

  // UDP
  udp.beginPacket(collectorIP, collectorPort);
  udp.write((const uint8_t*)jsonBuf, jsonLen);
  udp.endPacket();

  // CSV to SD: uptime_ms,learning,vib_mean_g,vib_kurt_ex,curr_rms_A,temp_slope_C_per_min,rpm_var,score
  if (logFile) {
    logFile.print(millis()); logFile.print(',');
    logFile.print(learning ? 1 : 0); logFile.print(',');
    logFile.print(vib_mean, 6); logFile.print(',');
    logFile.print(vib_kurt, 6); logFile.print(',');
    logFile.print(cur_rms, 6); logFile.print(',');
    logFile.print(temp_slope, 6); logFile.print(',');
    logFile.print(rpm_var, 6); logFile.print(',');
    logFile.println(anomaly_score, 6);
    logFile.flush();
  }

  // Slide window: here we drop all (tumbling window). Alternative: overlap.
  winCount = 0;
}

void setup() {
  pinMode(PIN_SS_MEGA, OUTPUT); // ensure SPI master
  pinMode(PIN_CS_ETH, OUTPUT);
  pinMode(PIN_CS_SD, OUTPUT);
  pinMode(PIN_CS_CAN, OUTPUT);
  digitalWrite(PIN_CS_ETH, HIGH);
  digitalWrite(PIN_CS_SD, HIGH);
  digitalWrite(PIN_CS_CAN, HIGH);

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

  // Ethernet init
  Serial.println(F("[ETH] Starting DHCP..."));
  int ethOK = Ethernet.begin(mac); // DHCP
  if (!ethOK) {
    Serial.println(F("[ETH] DHCP failed. Using static IP."));
    Ethernet.begin(mac, staticIP, staticDNS, staticGW, staticMask);
  }
  delay(1000);
  IPAddress ip = Ethernet.localIP();
  Serial.print(F("[ETH] IP: ")); Serial.println(ip);
  udp.begin(0); // ephemeral local port

  // SD init
  Serial.print(F("[SD] Initializing... "));
  if (!SD.begin(PIN_CS_SD)) {
    Serial.println(F("FAILED (continuing without SD)."));
  } else {
    Serial.println(F("OK."));
    // Create/open CSV and header if new
    if (!SD.exists(csvName)) {
      logFile = SD.open(csvName, FILE_WRITE);
      if (logFile) {
        logFile.println("uptime_ms,learning,vib_mean_g,vib_kurt_ex,curr_rms_A,temp_slope_C_per_min,rpm_var,score");
        logFile.flush();
      }
    } else {
      logFile = SD.open(csvName, FILE_WRITE);
    }
  }

  // CAN init
  pinMode(PIN_INT_CAN, INPUT); // MCP2515 INT is open-drain, pulled-up internally on shield
  Serial.print(F("[CAN] Initializing MCP2515... "));
#if USE_LOOPBACK
  if (CAN0.begin(MCP_ANY, CAN_BAUD, MCP_16MHZ) == CAN_OK) {
    CAN0.setMode(MCP_LOOPBACK);
    Serial.println(F("OK (LOOPBACK)."));
  } else {
    Serial.println(F("FAILED."));
  }
#else
  if (CAN0.begin(MCP_ANY, CAN_BAUD, MCP_16MHZ) == CAN_OK) {
    CAN0.setMode(MCP_NORMAL);
    Serial.println(F("OK (NORMAL)."));
  } else {
    Serial.println(F("FAILED."));
  }
#endif
}

void loop() {
#if USE_LOOPBACK
  generateSyntheticCAN();
#endif

  // Read all pending CAN frames
  while (digitalRead(PIN_INT_CAN) == LOW) {
    unsigned long id;
    unsigned char len;
    unsigned char buf[8];
    if (CAN0.readMsgBuf(&id, &len, buf) == CAN_OK) {
      handleCANFrame(id, len, buf);
    } else {
      break;
    }
  }

  // Periodically process the window when it's full
  if (winCount >= WINDOW_SIZE) {
    processWindow();
  }
}

Notes about the code:

  • USE_LOOPBACK = 1 lets you validate without a live CAN bus. The MCP2515 loopback mode receives frames we generate and transmits.
  • Change CAN speed from 500 kbps if your real bus uses a different rate (e.g., CAN_250KBPS).
  • We use tumbling windows (non-overlapping) for simpler timing. For higher update rate, implement overlapping windows.
  • UDP JSON lines allow network collection; CSV file on SD gives local audit.

Build/Flash/Run commands

We will use Arduino CLI with arduino:avr core targeted at Mega 2560.

1) Install/Update core index and AVR core:

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

2) Install required libraries at specific versions:

arduino-cli lib install "MCP_CAN_lib@1.5.1"
arduino-cli lib install "Ethernet@2.0.2"
arduino-cli lib install "SD@1.2.4"
arduino-cli lib install "ArduinoJson@6.21.3"

3) Verify your serial port:
– Linux/macOS: ls /dev/ttyACM or ls /dev/tty.usbmodem
– Windows: Check Device Manager for COMx (e.g., COM3)

4) Compile:

arduino-cli compile --fqbn arduino:avr:mega ~/Arduino/can-predictive-maintenance-logger

5) Upload (pick your port):

  • Linux/macOS example:
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega ~/Arduino/can-predictive-maintenance-logger
  • Windows example:
arduino-cli upload -p COM3 --fqbn arduino:avr:mega %HOMEPATH%\Documents\Arduino\can-predictive-maintenance-logger

6) Open Serial Monitor (115200 baud):

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

Step-by-step Validation

Follow these steps to validate progressively, first in loopback mode, then on a live CAN bus.

1) Power-on and SPI sanity

  • With the shields stacked, connect the Arduino Mega 2560 via USB.
  • Ensure:
  • D53 is OUTPUT (handled in code).
  • CS pins D10 (ETH), D4 (SD), and D9 (CAN) are distinct.
  • INT pin D2 is connected.

On Serial Monitor, expect lines like:

  • [ETH] Starting DHCP…
  • [ETH] IP: 192.168.1.X
  • [SD] Initializing… OK.
  • [CAN] Initializing MCP2515… OK (LOOPBACK).

If DHCP fails, you’ll see static IP being used.

2) UDP collector on your PC

Run a UDP listener on the same LAN:

  • Linux/macOS (netcat):
nc -ul 5000
  • Windows (PowerShell with ncat from Nmap installed):
ncat.exe -ul 5000

You should see JSON lines every time a window completes (after 60 synthetic samples), for example:

{«uptime_ms»:123456,»learning»:true,»win»:60,»feat»:{«vib_mean_g»:0.2531,»vib_kurt_ex»:-1.21,»curr_rms_A»:2.034,»temp_slope_C_per_min»:0.12,»rpm_var»:420.3},»score»:0.84}

Interpretation:

  • learning true during the first 10 windows; afterwards false.
  • score gradually stabilizes around a low value; spikes indicate anomalies.

3) CSV on SD

Remove the microSD after a few minutes and check pm_log.csv. You should see:

uptime_ms,learning,vib_mean_g,vib_kurt_ex,curr_rms_A,temp_slope_C_per_min,rpm_var,score
… lines of comma-separated values …

Open in a spreadsheet or parse with your scripts for trending.

4) Loopback mode behavior

With USE_LOOPBACK = 1, the sketch generates synthetic frames:

  • Every ~100 ms it pushes a set of four IDs (0x200..0x203).
  • Every ~60 samples, one feature window is computed and logged.
  • Every ~1 minute, the generator injects a vibration anomaly—observe a larger score.

If you want faster validation, reduce WINDOW_SIZE to e.g., 20.

5) Live CAN bus validation (optional)

Switch to real receive mode:

  • In the sketch, set USE_LOOPBACK to 0.
  • Ensure your bus runs at CAN_500KBPS or adjust CAN_BAUD to match your network.
  • Verify termination: exactly two 120 Ω at bus ends; disable on-board termination if the shield is not at the end.
  • Connect to the bus:
  • DB9 pin 7 -> CAN_H
  • DB9 pin 2 -> CAN_L
  • DB9 pin 3 -> GND

If you have a USB-to-CAN on a Linux PC with SocketCAN:

  • Bring up interface (example for can0 at 500 kbps):
sudo ip link set can0 up type can bitrate 500000
candump can0
  • Inject a test frame (example: vibration 350 mg):
cansend can0 200#5E01

Explanation: 0x015E = 350 decimal; bytes little-endian => 5E 01.

  • Similarly inject temp, current, rpm:
cansend can0 201#E803     # 100.0 C (0x03E8 = 1000 dC)
cansend can0 202#F401     # 50.0 A (0x01F4 = 500 dA)
cansend can0 203#DC05     # 1500 RPM (0x05DC)

As frames arrive, the Arduino accumulates samples and outputs windows/score. Check both UDP and Serial logs.

6) Feature/anomaly validation

  • Baseline learning: for the first 10 windows, «learning» is true. The baseline means and variances are being established; scores should be small unless your bus values are extreme.
  • Inject an anomaly:
  • Increase vibration or current significantly for several samples; expect a jump in score dominated by z_vib_mean or z_curr_rms.
  • Heat ramp: maintain a steady positive temperature slope; z_temp_slope should rise.

Observe the per-feature values in the JSON (feat object) alongside the score to verify the scoring is sensible.


Troubleshooting

  • No Ethernet IP:
  • Confirm your router offers DHCP. If not, confirm static IP range matches your LAN.
  • Ensure CS lines: D10 HIGH except when Ethernet active (library handles this). No other device should hold MISO low—check other CS pins are HIGH.
  • Use known-good Ethernet cable and switch port.

  • SD initialization fails:

  • Ensure a FAT32-formatted card; try 4–32 GB.
  • CS must be D4; confirm no solder/jumper conflicts on shields.
  • Confirm SPI bus works by testing Ethernet separately.

  • CAN initialization fails:

  • Verify the MCP2515 oscillator setting is MCP_16MHZ in code (as provided).
  • Ensure D9 is wired for CS and D2 for INT (default on Seeed Shield V2).
  • If on a live bus, mismatch in bitrate causes receive failures. Adjust CAN_BAUD to CAN_250KBPS or your bus speed.

  • No CAN frames in live mode:

  • Check termination: exactly two 120 Ω ends. Do not enable the shield’s termination if your bus already has two.
  • Confirm wiring polarity: CAN_H to H, CAN_L to L.
  • Some networks require specific IDs or filters. We use MCP_ANY to accept all frames; if frames are proprietary, adjust ID mapping.

  • SPI conflicts among W5500, SD, MCP2515:

  • Each device must have its CS pin set to HIGH when not in use. The sketch sets all CS pins HIGH at boot.
  • If modifying code, never access more than one SPI slave at the same time without deasserting the previous CS.

  • Serial is garbled:

  • Ensure monitor at 115200 baud.

  • Memory constraints:

  • Mega 2560 has more SRAM than UNO, but still be mindful. We use StaticJsonDocument<320> to avoid heap allocations.

Improvements

  • Timestamping:
  • Add SNTP client to set a real UNIX timestamp and include it in JSON/CSV. W5500 cannot do TLS; use UDP SNTP (simple) or relay to a gateway that adds secure transport.
  • MQTT publishing:
  • Push JSON to a local MQTT broker via UDP-to-MQTT bridge or switch to a different network stack that supports TCP + TLS offload if required (not available on W5500 alone).
  • Feature engineering:
  • Add spectral features (band energy ratios) by computing a short FFT on vibration values if you can ingest a high-rate signal via CAN.
  • Use exponentially weighted moving averages (EWMA) to track drift separately from variance.
  • Model sophistication:
  • Use Mahalanobis distance over a feature vector with covariance, rather than independent z-scores.
  • Learn separate baselines per operating regime (e.g., low vs. high RPM clusters).
  • Windowing strategy:
  • Overlapping windows to increase temporal resolution.
  • Adaptive window sizes based on RPM or load.
  • Reliability:
  • File rotation on SD (e.g., daily files pm_YYYYMMDD.csv).
  • Watchdog timer, brown-out detection, graceful SD sync.
  • Device management:
  • Add a small command interface over Serial or UDP (e.g., set loopback, set window size, set collector IP/port).
  • CAN filters:
  • Program MCP2515 masks/filters to accept only needed IDs for load reduction.

Final Checklist

  • Hardware
  • Arduino Mega 2560 stacked with W5500 Ethernet Shield and Seeed CAN-BUS Shield V2
  • CS lines: D10 (ETH), D4 (SD), D9 (CAN), INT on D2
  • D53 set to OUTPUT (SPI master)
  • Proper CAN termination (two 120 Ω at bus ends)
  • microSD inserted, FAT32

  • Software

  • Arduino CLI installed
  • arduino:avr core installed
  • Libraries installed at specified versions:
    • MCP_CAN_lib@1.5.1
    • Ethernet@2.0.2
    • SD@1.2.4
    • ArduinoJson@6.21.3
  • Sketch compiled with FQBN arduino:avr:mega

  • Build/Flash

  • Compile: arduino-cli compile –fqbn arduino:avr:mega ~/Arduino/can-predictive-maintenance-logger
  • Upload: arduino-cli upload -p –fqbn arduino:avr:mega ~/Arduino/can-predictive-maintenance-logger

  • Validation

  • Serial shows Ethernet IP, SD OK, CAN OK (LOOPBACK or NORMAL)
  • UDP listener receiving JSON lines on port 5000
  • SD pm_log.csv contains header and rows
  • In loopback mode: periodic anomaly spikes visible in score
  • In live mode: stable baseline, score reacts to injected anomalies

With this setup, you have a complete, field-deployable CAN predictive maintenance logger based on Arduino Mega 2560, Seeed MCP2515 CAN-BUS Shield V2, and a W5500 Ethernet Shield, ready to stream features and anomaly scores in real time while persisting to local storage.

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

Go to Amazon

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

Quick Quiz

Question 1: What microcontroller is used in the predictive maintenance logger?




Question 2: Which shield is used for CAN communication in this project?




Question 3: What type of Ethernet shield is compatible with the Arduino Mega 2560?




Question 4: What programming language is required for this project?




Question 5: What is the purpose of the predictive maintenance logger?




Question 6: What type of card is recommended for logging data?




Question 7: What is the recommended size of the microSD card?




Question 8: What is used to validate CAN frames externally?




Question 9: What is required for the CAN bus wiring?




Question 10: What is the termination resistance needed at each end of the CAN bus?




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

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

Follow me: