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:
error: Contenido Protegido / Content is protected !!
Scroll to Top