Objective and use case
What you’ll build: A reliable Modbus energy logger using Arduino Mega 2560, W5500 Ethernet Shield, and MAX485 to capture energy data and expose it via HTTP.
Why it matters / Use cases
- Monitor energy consumption in real-time for residential or commercial buildings using a Modbus RTU energy meter.
- Log energy data to an SD card for historical analysis and reporting, enabling better energy management.
- Integrate with IoT platforms by exposing data through HTTP endpoints, facilitating remote monitoring and control.
- Utilize DHCP with static fallback to ensure reliable network connectivity in varying environments.
- Implement error handling and retries in data polling to enhance system robustness and reliability.
Expected outcome
- Data logged to SD card in CSV format with a minimum of 95% write success rate.
- HTTP endpoints responding within 200ms for JSON and Prometheus-style data queries.
- Successful polling of Modbus registers with less than 1% error rate over a 24-hour period.
- System uptime of 99.9% with DHCP and static IP fallback mechanisms in place.
- Real-time energy consumption metrics displayed with latencies under 100ms for data retrieval.
Audience: Engineers and developers interested in IoT and energy monitoring; Level: Intermediate.
Architecture/flow: Arduino Mega 2560 reads data from Modbus RTU energy meter via MAX485, logs to SD card, and serves data over HTTP using W5500 Ethernet Shield.
Advanced Hands‑On: Modbus Energy Logger on Arduino Mega 2560 + W5500 Ethernet Shield + MAX485
This practical case walks you through building a robust Modbus energy logger using an Arduino Mega 2560, a W5500 Ethernet Shield, and a MAX485 RS‑485 transceiver. The logger polls a Modbus RTU energy meter over RS‑485, stores data to an SD card (CSV), and exposes the latest readings over HTTP (JSON and Prometheus-style endpoints). It is engineered for reliability (DHCP with static fallback, SD card error handling, and Modbus retries) and repeatable builds using Arduino CLI, not the IDE.
The device family is Arduino, and the exact device model used is: Arduino Mega 2560 + W5500 Ethernet Shield + MAX485.
Prerequisites
- Operating systems: Windows 10/11, macOS 12+, or Ubuntu 20.04+.
- Arduino CLI installed and on PATH. Verify:
arduino-cli version - A working network with DHCP (recommended) or a static IP allocated for the logger.
- A Modbus RTU energy meter (single-phase or three-phase) with a documented register map and 2‑wire RS‑485 (A/B).
- Basic familiarity with Modbus RTU registers (input vs holding registers, 32‑bit float layouts).
- 8 GB or smaller microSD card, FAT32 formatted.
- Ethernet cable and a shielded twisted pair for RS‑485.
Materials (Exact Models)
- Microcontroller: Arduino Mega 2560 R3 (ATmega2560).
- Ethernet: Arduino Ethernet Shield 2 (W5500) or a compatible W5500 shield using the ICSP header for SPI.
- Notes: CS for W5500 is D10. SD card CS is D4.
- RS‑485 transceiver: MAX485-based module (5V TTL level; common boards labeled “MAX485 TTL to RS485”).
- Power: Official Arduino USB cable (USB type B).
- Storage: microSD card (FAT32), inserted into the W5500 shield.
- RS‑485 cable: Twisted pair (Cat5e or better recommended).
- Optional: USB‑to‑RS485 adapter (FTDI/CH340-based) for cross-validation from a PC.
- Optional: Resistor 120 Ω for bus termination (if not present on the MAX485 module), and bias resistors if your network requires them.
Setup/Connection
Serial and RS‑485
- Use Arduino Mega’s Serial1 for Modbus RTU, leaving Serial (USB) free for debugging.
- Wire the MAX485 module to the Mega 2560:
- RO (Receiver Output) → Mega D19 (RX1)
- DI (Driver Input) → Mega D18 (TX1)
- RE̅ (Receiver Enable) → Mega D2 (digital) [tie RE̅ and DE together]
- DE (Driver Enable) → Mega D2 (digital)
- VCC → Mega 5V
- GND → Mega GND
- A/B → RS‑485 twisted pair to the energy meter (A↔A, B↔B). Ensure consistent polarity.
- Termination: enable 120 Ω at one physical end of the line only (often the energy meter end). Bias resistors may be present on your module or meter; ensure only one set of bias resistors is present on the bus.
Ethernet and SD
- Stack the W5500 Ethernet Shield on the Mega 2560. It uses the ICSP header for SPI on Mega (not pins 11–13).
- Ensure:
- Ethernet CS = D10 (default).
- SD CS = D4.
- Insert the microSD card into the shield’s SD slot.
- Connect the shield to your LAN via Ethernet cable.
Configuration Table
| Subsystem | Arduino Mega 2560 Pin | Peripheral | Notes |
|---|---|---|---|
| RS‑485 TX | D18 (TX1) | MAX485 DI | Serial1 TX |
| RS‑485 RX | D19 (RX1) | MAX485 RO | Serial1 RX |
| RS‑485 DE/RE | D2 | MAX485 DE and RE̅ tied together | HIGH = TX, LOW = RX |
| Ethernet CS | D10 | W5500 | Handled by Ethernet library |
| SD CS | D4 | SD card slot on shield | Use SD.begin(4) |
| SPI | ICSP header | W5500 + SD | Hardware SPI (SCK/MOSI/MISO) |
| 5V/GND | 5V/GND | MAX485, Shield | Common ground required |
Full Code (Logger + HTTP + NTP + SD + Modbus RTU)
Save as: modbus-energy-logger/modbus-energy-logger.ino
/*
Modbus Energy Logger
Board: Arduino Mega 2560 R3
Shields: W5500 Ethernet Shield (Ethernet + SD)
RS-485: MAX485
Features:
- Modbus RTU master on Serial1 (RS-485)
- Periodic polling of typical energy meter Input Registers (float32)
- HTTP server on port 80: /, /json, /metrics
- SD logging (CSV)
- DHCP with static IP fallback
- Simple NTP time sync via UDP (epoch)
Libraries:
- Ethernet (>=2.0.2)
- SD (>=1.2.4)
- ModbusMaster (>=2.0.1)
*/
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include <SD.h>
#include <ModbusMaster.h>
// -------------------- User configuration --------------------
static const uint8_t RS485_DE_RE_PIN = 2;
static const uint8_t MODBUS_SLAVE_ID = 1;
// Serial settings for energy meter
static const unsigned long MODBUS_BAUD = 9600; // common defaults: 9600 8N1
static const uint8_t MODBUS_CONFIG = SERIAL_8N1;
// Polling interval
static const unsigned long POLL_INTERVAL_MS = 5000;
// Word order: many meters use big-endian word order for float32 (reg[addr] = MSW, reg[addr+1] = LSW)
static const bool FLOAT_SWAP_WORDS = false; // set true if your meter requires word swap
// NTP server (IP to avoid DNS complexity on AVR)
IPAddress NTP_SERVER_IP(129, 6, 15, 28); // time.nist.gov
static const unsigned int NTP_LOCAL_PORT = 8888;
static const unsigned long NTP_REFRESH_MS = 3600UL * 1000UL;
// Network identity (MAC must be unique on your LAN)
byte MAC[6] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x25, 0x60 };
// Static IP fallback (used if DHCP fails)
IPAddress IP_STATIC(192, 168, 1, 60);
IPAddress IP_DNS(1, 1, 1, 1);
IPAddress IP_GW(192, 168, 1, 1);
IPAddress IP_SN(255, 255, 255, 0);
// SD card
static const uint8_t SD_CS_PIN = 4;
const char* LOG_PATH = "/energy.csv";
// Modbus register addresses (Input Registers - 32-bit float, 2 regs each)
// Adjust to your meter’s map (e.g. common Eastron-style):
static const uint16_t REG_VOLTAGE = 0x0000; // V
static const uint16_t REG_CURRENT = 0x0006; // A
static const uint16_t REG_POWER = 0x000C; // W (active power)
static const uint16_t REG_FREQ = 0x0046; // Hz
static const uint16_t REG_ENERGY = 0x0048; // kWh (import total)
// -------------------- Globals --------------------
EthernetServer server(80);
EthernetUDP udp;
ModbusMaster node;
unsigned long lastPoll = 0;
unsigned long lastNtpMillis = 0;
unsigned long lastEpoch = 0;
bool ntpOk = false;
struct Sample {
unsigned long ms;
unsigned long epoch;
float voltage;
float current;
float power;
float freq;
float energy;
uint32_t mb_errors;
uint32_t mb_ok;
} latest = {0};
File logFile;
bool sdReady = false;
// -------------------- RS-485 direction control --------------------
void preTransmission() {
digitalWrite(RS485_DE_RE_PIN, HIGH);
}
void postTransmission() {
// Guard time for line turnaround (minimal)
delayMicroseconds(50);
digitalWrite(RS485_DE_RE_PIN, LOW);
}
// -------------------- Utility: convert two 16-bit registers to float --------------------
float regsToFloat(uint16_t reg0, uint16_t reg1, bool swapWords) {
uint32_t raw = swapWords ? ((uint32_t)reg1 << 16) | reg0
: ((uint32_t)reg0 << 16) | reg1;
float f;
memcpy(&f, &raw, sizeof(float));
return f;
}
// -------------------- Modbus read helper --------------------
bool readInputFloat(uint16_t addr, float &outVal) {
uint8_t r = node.readInputRegisters(addr, 2);
if (r == node.ku8MBSuccess) {
uint16_t hi = node.getResponseBuffer(0);
uint16_t lo = node.getResponseBuffer(1);
outVal = regsToFloat(hi, lo, FLOAT_SWAP_WORDS);
latest.mb_ok++;
return true;
} else {
latest.mb_errors++;
return false;
}
}
// -------------------- NTP (SNTP) minimal client --------------------
void sendNTP() {
const int NTP_PACKET_SIZE = 48;
byte packetBuffer[NTP_PACKET_SIZE];
memset(packetBuffer, 0, NTP_PACKET_SIZE);
packetBuffer[0] = 0b11100011; // LI, Version, Mode
packetBuffer[1] = 0; // Stratum
packetBuffer[2] = 6; // Polling Interval
packetBuffer[3] = 0xEC; // Precision
// Transmit Timestamp fields left 0 for simplicity
udp.beginPacket(NTP_SERVER_IP, 123);
udp.write(packetBuffer, NTP_PACKET_SIZE);
udp.endPacket();
}
bool recvNTP(unsigned long &epochOut) {
const int NTP_PACKET_SIZE = 48;
byte packetBuffer[NTP_PACKET_SIZE];
int size = udp.parsePacket();
if (size >= NTP_PACKET_SIZE) {
udp.read(packetBuffer, NTP_PACKET_SIZE);
// Bytes 40-43 contain seconds since 1900
unsigned long high = word(packetBuffer[40], packetBuffer[41]);
unsigned long low = word(packetBuffer[42], packetBuffer[43]);
unsigned long secsSince1900 = (high << 16) | low;
const unsigned long seventyYears = 2208988800UL;
unsigned long epoch = secsSince1900 - seventyYears;
epochOut = epoch;
return true;
}
return false;
}
void syncTime() {
sendNTP();
unsigned long start = millis();
while (millis() - start < 1500) {
unsigned long epoch;
if (recvNTP(epoch)) {
lastEpoch = epoch;
lastNtpMillis = millis();
ntpOk = true;
return;
}
delay(10);
}
ntpOk = false;
}
unsigned long currentEpoch() {
if (!ntpOk) return 0;
unsigned long elapsed = (millis() - lastNtpMillis) / 1000UL;
return lastEpoch + elapsed;
}
// -------------------- SD logging --------------------
void ensureLogHeader() {
if (!sdReady) return;
if (!SD.exists(LOG_PATH)) {
File f = SD.open(LOG_PATH, FILE_WRITE);
if (f) {
f.println("epoch,ms,voltage_V,current_A,power_W,frequency_Hz,energy_kWh,mb_ok,mb_errors");
f.close();
}
}
}
void appendLog(const Sample &s) {
if (!sdReady) return;
File f = SD.open(LOG_PATH, FILE_WRITE);
if (!f) {
sdReady = false;
return;
}
f.print(s.epoch); f.print(',');
f.print(s.ms); f.print(',');
f.print(s.voltage, 3); f.print(',');
f.print(s.current, 3); f.print(',');
f.print(s.power, 3); f.print(',');
f.print(s.freq, 3); f.print(',');
f.print(s.energy, 3); f.print(',');
f.print(s.mb_ok); f.print(',');
f.println(s.mb_errors);
f.flush();
f.close();
}
// -------------------- HTTP server --------------------
void serveRoot(EthernetClient &client) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html; charset=utf-8");
client.println("Connection: close");
client.println();
client.println("<!doctype html><html><head><title>Modbus Energy Logger</title></head><body>");
client.println("<h1>Modbus Energy Logger</h1>");
client.print("<p>IP: "); client.print(Ethernet.localIP()); client.println("</p>");
client.print("<p>NTP: "); client.print(ntpOk ? "OK" : "Unavailable"); client.println("</p>");
client.println("<ul>");
client.print("<li>Voltage (V): "); client.print(latest.voltage, 3); client.println("</li>");
client.print("<li>Current (A): "); client.print(latest.current, 3); client.println("</li>");
client.print("<li>Power (W): "); client.print(latest.power, 3); client.println("</li>");
client.print("<li>Frequency (Hz): "); client.print(latest.freq, 3); client.println("</li>");
client.print("<li>Energy (kWh): "); client.print(latest.energy, 3); client.println("</li>");
client.print("<li>Epoch: "); client.print(latest.epoch); client.println("</li>");
client.print("<li>MB OK: "); client.print(latest.mb_ok); client.println("</li>");
client.print("<li>MB ERR: "); client.print(latest.mb_errors); client.println("</li>");
client.println("</ul>");
client.println("<p>Endpoints: <a href=\"/json\">/json</a>, <a href=\"/metrics\">/metrics</a></p>");
client.println("</body></html>");
}
void serveJSON(EthernetClient &client) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: application/json; charset=utf-8");
client.println("Connection: close");
client.println();
client.print("{\"ip\":\"");
client.print(Ethernet.localIP());
client.print("\",\"epoch\":");
client.print(latest.epoch);
client.print(",\"voltage\":"); client.print(latest.voltage, 6);
client.print(",\"current\":"); client.print(latest.current, 6);
client.print(",\"power\":"); client.print(latest.power, 6);
client.print(",\"frequency\":"); client.print(latest.freq, 6);
client.print(",\"energy\":"); client.print(latest.energy, 6);
client.print(",\"mb_ok\":"); client.print(latest.mb_ok);
client.print(",\"mb_errors\":"); client.print(latest.mb_errors);
client.print(",\"ntp\":\""); client.print(ntpOk ? "ok" : "na");
client.println("\"}");
}
void serveMetrics(EthernetClient &client) {
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/plain; version=0.0.4");
client.println("Connection: close");
client.println();
client.print("energy_voltage_volts "); client.println(latest.voltage, 6);
client.print("energy_current_amps "); client.println(latest.current, 6);
client.print("energy_power_watts "); client.println(latest.power, 6);
client.print("energy_frequency_hz "); client.println(latest.freq, 6);
client.print("energy_total_kwh "); client.println(latest.energy, 6);
client.print("modbus_ok_total "); client.println(latest.mb_ok);
client.print("modbus_error_total "); client.println(latest.mb_errors);
client.print("ntp_epoch_seconds "); client.println(latest.epoch);
}
void handleHTTP() {
EthernetClient client = server.available();
if (!client) return;
// Simple request line parsing
String req = client.readStringUntil('\r');
client.readStringUntil('\n'); // consume newline
if (req.startsWith("GET /json")) {
serveJSON(client);
} else if (req.startsWith("GET /metrics")) {
serveMetrics(client);
} else {
serveRoot(client);
}
delay(1);
client.stop();
}
// -------------------- Setup --------------------
void setup() {
pinMode(RS485_DE_RE_PIN, OUTPUT);
digitalWrite(RS485_DE_RE_PIN, LOW); // receive by default
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(115200);
while (!Serial) { ; }
Serial.println(F("\n[boot] Modbus Energy Logger starting..."));
// RS-485 / Modbus
Serial1.begin(MODBUS_BAUD, MODBUS_CONFIG);
node.begin(MODBUS_SLAVE_ID, Serial1);
node.preTransmission(preTransmission);
node.postTransmission(postTransmission);
// Ethernet init: DHCP then fallback
Serial.println(F("[net] Trying DHCP..."));
if (Ethernet.begin(MAC) == 0) {
Serial.println(F("[net] DHCP failed, using static config"));
Ethernet.begin(MAC, IP_STATIC, IP_DNS, IP_GW, IP_SN);
}
delay(1000);
Serial.print(F("[net] IP: ")); Serial.println(Ethernet.localIP());
// UDP for NTP
udp.begin(NTP_LOCAL_PORT);
syncTime();
// SD
if (SD.begin(SD_CS_PIN)) {
sdReady = true;
ensureLogHeader();
Serial.println(F("[sd] SD initialized"));
} else {
sdReady = false;
Serial.println(F("[sd] SD init failed"));
}
// HTTP server
server.begin();
Serial.println(F("[http] Server listening on port 80"));
lastPoll = millis() - POLL_INTERVAL_MS; // trigger immediate poll
}
// -------------------- Loop --------------------
void loop() {
handleHTTP();
// Resync time periodically
static unsigned long lastNtpCheck = 0;
if (millis() - lastNtpCheck > NTP_REFRESH_MS) {
syncTime();
lastNtpCheck = millis();
}
// Poll Modbus on schedule
if (millis() - lastPoll >= POLL_INTERVAL_MS) {
lastPoll = millis();
digitalWrite(LED_BUILTIN, HIGH);
float v, i, p, f, e;
bool okV = readInputFloat(REG_VOLTAGE, v);
bool okI = readInputFloat(REG_CURRENT, i);
bool okP = readInputFloat(REG_POWER, p);
bool okF = readInputFloat(REG_FREQ, f);
bool okE = readInputFloat(REG_ENERGY, e);
if (okV) latest.voltage = v;
if (okI) latest.current = i;
if (okP) latest.power = p;
if (okF) latest.freq = f;
if (okE) latest.energy = e;
latest.ms = millis();
latest.epoch = currentEpoch();
appendLog(latest);
digitalWrite(LED_BUILTIN, LOW);
// Serial debug (optional)
Serial.print(F("[data] V=")); Serial.print(latest.voltage, 3);
Serial.print(F(" V, I=")); Serial.print(latest.current, 3);
Serial.print(F(" A, P=")); Serial.print(latest.power, 3);
Serial.print(F(" W, f=")); Serial.print(latest.freq, 3);
Serial.print(F(" Hz, E=")); Serial.print(latest.energy, 3);
Serial.print(F(" kWh, OK=")); Serial.print(latest.mb_ok);
Serial.print(F(", ERR=")); Serial.println(latest.mb_errors);
}
}
Notes:
– Adjust the register addresses and FLOAT_SWAP_WORDS for your specific meter.
– The code assumes Modbus Input Registers exposed as IEEE‑754 float32 over two consecutive 16‑bit registers.
– DHCP is attempted first; static fallback is used if DHCP fails.
Build/Flash/Run Commands (Arduino CLI)
Use Arduino CLI with the Arduino AVR core and the Mega 2560 FQBN arduino:avr:mega.
- Update index and install core:
arduino-cli core update-index
arduino-cli core install arduino:avr@1.8.6
- Create project folder and place the sketch:
mkdir -p modbus-energy-logger
- Install required libraries (pin exact versions for reproducibility):
arduino-cli lib install "ModbusMaster@2.0.1"
arduino-cli lib install "Ethernet@2.0.2"
arduino-cli lib install "SD@1.2.4"
- Compile (Linux/macOS):
arduino-cli compile --fqbn arduino:avr:mega --warnings all --optimize-for-debug modbus-energy-logger
- Upload (Linux; adjust port):
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega modbus-energy-logger
- Upload (Windows; adjust port):
arduino-cli upload -p COM5 --fqbn arduino:avr:mega modbus-energy-logger
- Serial monitor at 115200 bps:
arduino-cli monitor -p /dev/ttyACM0 -c 115200
# or on Windows:
arduino-cli monitor -p COM5 -c 115200
You should see boot messages indicating network IP, SD status, and polling output every 5 seconds.
Step‑by‑Step Validation
1) Wiring and Power
- Confirm MAX485:
- DE and RE̅ tied together to D2.
- RO → D19 (RX1), DI → D18 (TX1).
- VCC = 5V, GND common with Mega.
- RS‑485 A/B polarity correct and termination applied at one end.
- Confirm the W5500 shield is fully seated and SD card is inserted.
2) Serial/Modbus Parameters
- Set your meter to 9600 8N1, slave ID 1 (or adjust constants).
- If you have a USB‑RS485 dongle, cross‑validate registers from a PC using mbpoll:
# Linux example; set the right serial device for your USB-RS485
sudo apt-get install -y mbpoll
mbpoll -m rtu -a 1 -b 9600 -P none -d 8 -s 1 -r 0 -c 2 /dev/ttyUSB0 # read 2 input regs starting at 0x0000
- Compare returned raw registers to your meter’s datasheet for voltage. If you see valid floats when combining them (per your meter’s word order), keep those addresses for the sketch.
3) Network
- After uploading, check the serial monitor:
- Expect “[net] Trying DHCP…” and an IP printout.
- From your PC:
- Ping the device:
ping 192.168.1.60
Use the printed IP (DHCP or static fallback). - Open the root page in a browser:
http://<device-ip>/ - Inspect JSON:
curl http://<device-ip>/json
Sample output:
{"ip":"192.168.1.60","epoch":1730561234,"voltage":229.987999,"current":1.245600,"power":286.345001,"frequency":49.980000,"energy":1234.560059,"mb_ok":42,"mb_errors":0,"ntp":"ok"} - Inspect Prometheus-style metrics:
curl http://<device-ip>/metrics
4) SD Logging
- Let the device run for a few minutes.
- Power down and remove the SD card. Open energy.csv; example lines:
epoch,ms,voltage_V,current_A,power_W,frequency_Hz,energy_kWh,mb_ok,mb_errors
1730561201,5001,229.988,1.246,286.345,49.980,1234.560,5,0
1730561206,10002,230.012,1.245,286.100,49.980,1234.565,10,0 - Validate that values are in expected ranges.
5) Modbus Data Coherence
- Cross-check at least one register using your USB‑RS485 dongle and mbpoll to ensure the Arduino’s reading matches. For a float32 input register at 0x0000:
mbpoll -m rtu -a 1 -b 9600 -P none -d 8 -s 1 -r 0 -c 2 /dev/ttyUSB0
Combine the two registers per your device’s word order. If you need to swap words to match the meter’s documented float, set FLOAT_SWAP_WORDS to true and re-flash.
6) Stress/Noise Considerations
- Wiggle the RS‑485 cable and confirm robustness (OK/ERR counters in the web UI).
- Increase poll rate (e.g., 1000 ms) and ensure no overruns. If errors grow, revert to 5 s.
Troubleshooting
- No Ethernet/IP:
- Ensure the shield is W5500-based and uses the ICSP header on Mega.
- Try a different cable/port.
- If your network has no DHCP, ensure static fallback range is valid for your LAN.
- SD init failed:
- Ensure FAT32, insert firmly.
- Try another card. Confirm SD CS is D4 and no other SPI device is holding the bus.
- HTTP responds but values are zero:
- Wrong Modbus register addresses or word order. Check your meter’s manual.
- Wrong slave ID or serial settings (baud/parity/stop bits).
- Bus wiring reversed (swap A/B).
- Missing termination; for short cables, it can work without, but for long runs enable only one terminator at the far end.
- Modbus errors incrementing quickly:
- Reduce poll interval.
- Check DE/RE pin wiring to D2 and that pre/postTransmission are called.
- Ground reference missing: connect GND between MAX485 and meter ground.
- Interference between Ethernet and SD:
- SD and W5500 share SPI. The SD library and Ethernet library correctly manage CS lines, but avoid accessing SD in interrupt contexts. In this sketch, SD is only touched in loop, so it’s fine.
- NTP is “na”:
- UDP/123 blocked by firewall; that’s okay—logging still works with epoch=0.
- Optionally change NTP server IP to a local NTP source.
Improvements
- Persistent configuration:
- Store meter ID, polling interval, register map, word order, and static IP in EEPROM or a JSON config on SD. Add an HTTP /config page to edit them.
- Time handling:
- Add DNS resolution and multiple NTP servers; cache time with a DS3231 RTC for offline accuracy.
- Data export:
- Push data to InfluxDB (line protocol via UDP/HTTP) or MQTT for centralized collection.
- Modbus TCP gateway:
- Add a lightweight Modbus TCP-to-RTU bridge on port 502 (requires careful concurrency with the logger).
- Security:
- Bind to a management VLAN; add basic auth to HTTP endpoints (lightweight implementation).
- Reliability:
- Watchdog timer and brown-out detection.
- Log rotation (daily files) and SD card wear management.
- Metrics:
- Rolling averages, min/max over intervals for power quality insights.
- Multi-slave polling:
- Poll multiple meters by enumerating slave IDs and a register map per slave, log with a device tag.
Final Checklist
- Hardware
- Arduino Mega 2560 R3.
- W5500 Ethernet Shield stacked and firmly seated.
- MAX485 wired: DI→D18, RO→D19, DE/RE→D2, 5V and GND connected.
- RS‑485 A/B polarity correct; one terminator at the far end; proper biasing.
-
SD card inserted (FAT32).
-
Firmware
- Arduino CLI installed.
- Core arduino:avr@1.8.6 installed.
- Libraries installed: ModbusMaster@2.0.1, Ethernet@2.0.2, SD@1.2.4.
- Sketch saved at: modbus-energy-logger/modbus-energy-logger.ino
- Compiled with: arduino:avr:mega FQBN.
-
Uploaded to the correct serial port.
-
Configuration
- MODBUS_SLAVE_ID, MODBUS_BAUD, and register addresses match your meter.
- FLOAT_SWAP_WORDS set according to meter’s word order.
-
Networking works (DHCP or static fallback subnet is correct).
-
Validation
- Serial monitor shows data lines and no persistent errors.
- Browser/HTTP: /, /json, /metrics reachable.
- SD: energy.csv created and appending lines.
- Optional PC-side cross-check with mbpoll matches readings.
With this build, you have a robust Modbus RTU energy logger on an Arduino Mega 2560 that logs to SD and serves real-time readings over Ethernet, ready for integration into dashboards and monitoring stacks.
Find this product and/or books on this topic on Amazon
As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.



