Overview
In this project, we will build an IoT DC Energy Meter using an ESP32, which can measure voltage, current, power, and energy consumption in real time and display the results on a dynamic web dashboard. Earlier we build a simple DC Energy Meter with Arduino.
I recently came across a neat custom-made DC Power Meter Module from Mutex Embedded. This compact yet powerful board can measure DC parameters such as voltage (0–32 V range), current (up to 5 A), power, and cumulative energy consumption. It communicates easily over the I²C protocol, making it ideal for integration with the ESP32 for IoT applications.
For this guide, I’ll show you how to connect the module to an ESP32 board and set up a web server that displays live energy data. The webpage, built with HTML, CSS, and JavaScript, automatically updates the readings using AJAX, so you can view voltage, current, power, and energy in real time on any device connected to the same Wi-Fi network.
This project is a simple yet very practical way to create an IoT-enabled DC power monitoring system with ESP32. Whether you’re working on electronics, battery-powered projects, or renewable energy experiments, this solution makes it easy to monitor your system’s energy usage remotely through a clean and responsive web interface.
Bill of Materials
We need the following components to build this IoT DC Energy meter with ESP32.
| S.N. | Components Name | Quantity | Purchase Link |
|---|---|---|---|
| 1 | ESP32 Board | 1 | Amazon | AliExpress |
| 2 | DC Power Monitor Module | 1 | Mutex-Embedded |
| 3 | Connecting Wires | 10 | Amazon | AliExpress |
| 4 | Breadboard | 1 | Amazon | AliExpress |
DC Power Monitor Module
The DC Power Monitoring Module is a feature-rich solution for tracking voltage, current, power, and energy consumption across four independent channels. At its core is the ESP32-S2 microcontroller, ensuring high performance and IoT readiness. Current measurement is handled through precision high-wattage shunt resistors, while an onboard SD card slot enables real-time data logging. A CR1220 coin cell holder powers the real-time clock, allowing accurate time-stamping of measurements even when the main supply is off.
On the hardware side, the board includes a tiny rear connector that works with a dedicated power extension board, which provides a USB-to-serial interface, micro-USB port, and reset switch. With support for up to 32 V input voltage and 5 A current per channel, the module is well-suited for monitoring small to medium DC appliances or battery systems.
The module supports both I²C and Modbus RTU (UART/RS485) interfaces, making it compatible with a wide range of microcontrollers, including Arduino and ESP32. Data can also be logged to the SD card for up to a year, with weekly historical summaries available for long-term analysis. To simplify user experience, a free PC application is provided for configuration, live monitoring, and firmware updates.
Features & Specifications
- Voltage range: 0 – 32V
- Current range 0 – 5A
- 4 – Channels with common GND
- Serial Interface: I2C + UART Modbus
- RS-485 Bus extension
- Real-Time Clock
- Micro SD Card Data Logging
- PC Application
- Built for Industrial use
Useful Links
Building an IoT DC Energy Meter with ESP32
The DC Power Monitor Module supports both I²C and Modbus RTU communication protocols. In this project, we will be using the I²C interface to connect the module with an ESP32 and build our own IoT-enabled DC Energy Meter.
Hardware & Circuit
The wiring is very straightforward. Connect the VCC pin of the Power Monitor Module to the 3.3V pin on the ESP32, and connect the GND pin to the ESP32 GND. Then, link the communication lines by connecting the module’s SDA pin to the ESP32’s default GPIO 21 and the SCL pin to GPIO 22. These are the default I²C pins on most ESP32 development boards.
With just these four simple connections, the ESP32 can read voltage, current, power, and energy data directly from the module over the I²C bus and then serve the data on a local web dashboard.
Power Supply & Load Connection
To start using the DC Power Monitor Module, a DC source and a load need to be connected. The module provides four independent input/output channels labeled IN1–IN4 and OUT1–OUT4. For demonstration purposes, we will work with Channel 1.
Connecting the Power Source
You can use any suitable DC supply, such as a battery pack or a regulated DC adapter. Connect the positive terminal of the supply to the IN1 terminal, while the negative (ground) terminal should be linked to the GND pin on the module.
Connecting the Load
Next, connect the positive terminal of your load (for example, a DC motor, LED lamp, or another electronic circuit) to the OUT1 terminal. The negative terminal of the load must also be tied to the common ground of the module.
Once these simple connections are made, the module will continuously measure the input voltage and the current flowing through the load, and from these values, it will automatically calculate the power and accumulated energy for Channel 1.
I2C Library (MbI2C_ArduinoPort.h)
The developer of the DC Power Monitor Module has already provided a library to interface with this device. Although the original library was created for another microcontroller platform, it can be easily adapted for the ESP32 to communicate with the module over the I²C bus. This allows us to read real-time values of voltage, current, power, and energy directly from the module without building everything from scratch.
To get started, create a folder anywhere on your computer and name it iot_energy_meter. Inside this folder, create a new text file and save it as MbI2C_ArduinoPort.h. Copy the provided library code into this file and save it.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 |
#include <Arduino.h> #include <Wire.h> #include <stdint.h> #include <string.h> // -------- Original mb_i2c.h content (with include-guard adapted) -------- typedef bool (*I2cWriteCallback_t) (uint8_t slaveAddrs, uint8_t* pDataWr, uint8_t size); typedef bool (*I2cReadCallback_t) (uint8_t slaveAddrs, uint8_t* pDataRd, uint8_t size); typedef uint32_t (*GetMillisCallback_t) (void); typedef void (*DelayCallback_t) (uint32_t msDelay); typedef struct { uint8_t i2cAddress; I2cWriteCallback_t cbWrite; I2cReadCallback_t cbRead; GetMillisCallback_t cbGetMillis; DelayCallback_t cbDelay; } MbI2c_Init_t; typedef struct { MbI2c_Init_t init; bool isInitialized; } MbI2c_Handle_t; typedef enum { MbI2c_RegType_Holding, MbI2c_RegType_Input, } MbI2c_RegType_e; // -------- Constants from mb_i2c.c -------- #ifndef MB_I2C_READ_BUFFER_SIZE #define MB_I2C_READ_BUFFER_SIZE 128 #endif #ifndef MB_I2C_READ_DELAY #define MB_I2C_READ_DELAY 5 #endif #ifndef MB_I2C_READ_TIMEOUT #define MB_I2C_READ_TIMEOUT 100 #endif typedef enum { MbI2c_Cmd_WriteHolding = 1, MbI2c_Cmd_ReadHolding = 2, MbI2c_Cmd_ReadInput = 3 } MbI2c_Cmd_e; // SMBus CRC-8 lookup table static const uint8_t gLookupTableSmbus[256] = { 0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, 0x38, 0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d, 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65, 0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, 0xe0, 0xe7, 0xee, 0xe9, 0xfc, 0xfb, 0xf2, 0xf5, 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd, 0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, 0xa8, 0xaf, 0xa6, 0xa1, 0xb4, 0xb3, 0xba, 0xbd, 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2, 0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, 0xb7, 0xb0, 0xb9, 0xbe, 0xab, 0xac, 0xa5, 0xa2, 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a, 0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, 0x1f, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0d, 0x0a, 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42, 0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, 0x89, 0x8e, 0x87, 0x80, 0x95, 0x92, 0x9b, 0x9c, 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4, 0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, 0xc1, 0xc6, 0xcf, 0xc8, 0xdd, 0xda, 0xd3, 0xd4, 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c, 0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, 0x19, 0x1e, 0x17, 0x10, 0x05, 0x02, 0x0b, 0x0c, 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34, 0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, 0x76, 0x71, 0x78, 0x7f, 0x6a, 0x6d, 0x64, 0x63, 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b, 0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, 0xae, 0xa9, 0xa0, 0xa7, 0xb2, 0xb5, 0xbc, 0xbb, 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8d, 0x84, 0x83, 0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, 0xe6, 0xe1, 0xe8, 0xef, 0xfa, 0xfd, 0xf4, 0xf3 }; static uint8_t gReadBuffer[MB_I2C_READ_BUFFER_SIZE]; // -------- Internal functions from mb_i2c.c -------- static uint8_t mbI2c_smbus_calculate (uint8_t* pData, uint16_t size) { uint8_t crcByte = 0; for(uint16_t i = 0; i < size; i++) { crcByte = gLookupTableSmbus[crcByte ^ pData[i]]; } return crcByte; } static bool mbI2c_smbus_verify (uint8_t* pData, uint16_t size, uint8_t crc) { uint8_t crcByte = mbI2c_smbus_calculate(pData, size); return (crcByte == crc); } static bool mbI2c_readData (MbI2c_Handle_t* pHandle, uint16_t* pData, uint16_t len) { uint8_t lenBytes = len * 2; bool i2cStatus = false; uint32_t startTicks = pHandle->init.cbGetMillis(); do { if(pHandle->init.cbRead(pHandle->init.i2cAddress, gReadBuffer, lenBytes + 1)) { uint8_t crcValue = gReadBuffer[lenBytes]; bool crcState = mbI2c_smbus_verify(gReadBuffer, lenBytes, crcValue); if(crcState) { memcpy(pData, gReadBuffer, lenBytes); i2cStatus = true; break; } else { i2cStatus = false; } } else { i2cStatus = false; } } while ((pHandle->init.cbGetMillis() - startTicks) < MB_I2C_READ_TIMEOUT); return i2cStatus; } // -------- Public API from mb_i2c.c -------- static bool mbI2c_init (MbI2c_Init_t* pInit, MbI2c_Handle_t* pHandle) { if((pInit->cbWrite != NULL) && (pInit->cbRead != NULL) && (pInit->cbGetMillis != NULL) && (pInit->cbDelay != NULL)) { memcpy(&pHandle->init, pInit, sizeof(MbI2c_Init_t)); pHandle->isInitialized = true; return true; } pHandle->isInitialized = false; return false; } static bool mbI2c_writeHolding (MbI2c_Handle_t* pHandle, uint16_t regAddrs, uint16_t* pWriteBuf, uint16_t len) { if(!pHandle->isInitialized) { return false; } bool i2cStatus = true; uint8_t cmdPacket[5] = {0}; cmdPacket[0] = MbI2c_Cmd_WriteHolding; cmdPacket[1] = (regAddrs & 0xFF); cmdPacket[2] = ((regAddrs >> 8) & 0xFF); cmdPacket[3] = len; cmdPacket[4] = mbI2c_smbus_calculate(cmdPacket, 4); i2cStatus = pHandle->init.cbWrite(pHandle->init.i2cAddress, cmdPacket, 5); if(i2cStatus) { i2cStatus = pHandle->init.cbWrite(pHandle->init.i2cAddress, (uint8_t*)pWriteBuf, len * 2); } if(i2cStatus) { uint8_t crcValue = mbI2c_smbus_calculate((uint8_t*)pWriteBuf, len * 2); i2cStatus = pHandle->init.cbWrite(pHandle->init.i2cAddress, (uint8_t*)&crcValue, 1); } return i2cStatus; } static bool mbI2c_readHolding (MbI2c_Handle_t* pHandle, uint16_t regAddrs, uint16_t* pReadBuf, uint16_t len) { if(!pHandle->isInitialized) { return false; } uint8_t cmdPacket[5] = {0}; cmdPacket[0] = MbI2c_Cmd_ReadHolding; cmdPacket[1] = (regAddrs & 0xFF); cmdPacket[2] = ((regAddrs >> 8) & 0xFF); cmdPacket[3] = len; cmdPacket[4] = mbI2c_smbus_calculate(cmdPacket, 4); bool i2cStatus = pHandle->init.cbWrite(pHandle->init.i2cAddress, cmdPacket, 5); if(i2cStatus) { pHandle->init.cbDelay(MB_I2C_READ_DELAY); i2cStatus = mbI2c_readData(pHandle, pReadBuf, len); } return i2cStatus; } static bool mbI2c_readInput (MbI2c_Handle_t* pHandle, uint16_t regAddrs, uint16_t* pReadBuf, uint16_t len) { if(!pHandle->isInitialized) { return false; } uint8_t cmdPacket[5] = {0}; cmdPacket[0] = MbI2c_Cmd_ReadInput; cmdPacket[1] = (regAddrs & 0xFF); cmdPacket[2] = ((regAddrs >> 8) & 0xFF); cmdPacket[3] = len; cmdPacket[4] = mbI2c_smbus_calculate(cmdPacket, 4); bool i2cStatus = pHandle->init.cbWrite(pHandle->init.i2cAddress, cmdPacket, 5); if(i2cStatus) { pHandle->init.cbDelay(MB_I2C_READ_DELAY); i2cStatus = mbI2c_readData(pHandle, pReadBuf, len); } return i2cStatus; } static bool mbI2c_writeHoldingUint16 (MbI2c_Handle_t* pHandle, uint16_t regAddrs, uint16_t data) { return mbI2c_writeHolding(pHandle, regAddrs, &data, 1); } static bool mbI2c_writeHoldingInt16 (MbI2c_Handle_t* pHandle, uint16_t regAddrs, int16_t data) { return mbI2c_writeHolding(pHandle, regAddrs, (uint16_t*)&data, 1); } static bool mbI2c_writeHoldingUint32 (MbI2c_Handle_t* pHandle, uint16_t regAddrs, uint32_t data) { return mbI2c_writeHolding(pHandle, regAddrs, (uint16_t*)&data, 2); } static bool mbI2c_writeHoldingInt32 (MbI2c_Handle_t* pHandle, uint16_t regAddrs, int32_t data) { return mbI2c_writeHolding(pHandle, regAddrs, (uint16_t*)&data, 2); } static bool mbI2c_writeHoldingBool (MbI2c_Handle_t* pHandle, uint16_t regAddrs, bool data) { uint16_t data16 = data; return mbI2c_writeHolding(pHandle, regAddrs, &data16, 1); } static bool mbI2c_writeHoldingFloat (MbI2c_Handle_t* pHandle, uint16_t regAddrs, float data) { return mbI2c_writeHolding(pHandle, regAddrs, (uint16_t*)&data, 2); } static bool mbI2c_readUint16 (MbI2c_Handle_t* pHandle, uint16_t regAddrs, uint16_t* pReadData, MbI2c_RegType_e regType) { if(regType == MbI2c_RegType_Holding) { return mbI2c_readHolding(pHandle, regAddrs, pReadData, 1); } else { return mbI2c_readInput(pHandle, regAddrs, pReadData, 1); } } static bool mbI2c_readInt16 (MbI2c_Handle_t* pHandle, uint16_t regAddrs, int16_t* pReadData, MbI2c_RegType_e regType) { if(regType == MbI2c_RegType_Holding) { return mbI2c_readHolding(pHandle, regAddrs, (uint16_t*)pReadData, 1); } else { return mbI2c_readInput(pHandle, regAddrs, (uint16_t*)pReadData, 1); } } static bool mbI2c_readUint32 (MbI2c_Handle_t* pHandle, uint16_t regAddrs, uint32_t* pReadData, MbI2c_RegType_e regType) { if(regType == MbI2c_RegType_Holding) { return mbI2c_readHolding(pHandle, regAddrs, (uint16_t*)pReadData, 2); } else { return mbI2c_readInput(pHandle, regAddrs, (uint16_t*)pReadData, 2); } } static bool mbI2c_readInt32 (MbI2c_Handle_t* pHandle, uint16_t regAddrs, int32_t* pReadData, MbI2c_RegType_e regType) { if(regType == MbI2c_RegType_Holding) { return mbI2c_readHolding(pHandle, regAddrs, (uint16_t*)pReadData, 2); } else { return mbI2c_readInput(pHandle, regAddrs, (uint16_t*)pReadData, 2); } } static bool mbI2c_readBool (MbI2c_Handle_t* pHandle, uint16_t regAddrs, bool* pReadData, MbI2c_RegType_e regType) { uint16_t data16 = 0; bool status = true; if(regType == MbI2c_RegType_Holding) { status = mbI2c_readHolding(pHandle, regAddrs, &data16, 1); } else { status = mbI2c_readInput(pHandle, regAddrs, &data16, 1); } *pReadData = (bool)data16; return status; } static bool mbI2c_readFloat (MbI2c_Handle_t* pHandle, uint16_t regAddrs, float* pReadData, MbI2c_RegType_e regType) { if(regType == MbI2c_RegType_Holding) { return mbI2c_readHolding(pHandle, regAddrs, (uint16_t*)pReadData, 2); } else { return mbI2c_readInput(pHandle, regAddrs, (uint16_t*)pReadData, 2); } } // -------- Arduino Wire() callbacks (plug into MbI2c_Init_t) -------- static bool ArduinoI2C_Write(uint8_t addr7, uint8_t* pDataWr, uint8_t size) { Wire.beginTransmission(addr7); size_t n = Wire.write(pDataWr, size); uint8_t err = Wire.endTransmission(true); // send STOP return (err == 0) && (n == size); } static bool ArduinoI2C_Read(uint8_t addr7, uint8_t* pDataRd, uint8_t size) { // One request for 'size' bytes; then pull them out. // Note: classic AVR Wire has a 32-byte rx buffer limit. uint32_t start = millis(); uint8_t got = Wire.requestFrom((int)addr7, (int)size, (int)true); while ( (Wire.available() < got) && (millis() - start < MB_I2C_READ_TIMEOUT) ) { // wait briefly if somehow not all are queued yet delay(1); } if (got < size) { // If the peripheral returned fewer bytes, fill what we have and fail for (uint8_t i=0; i<got; ++i) pDataRd[i] = (uint8_t)Wire.read(); return false; } for (uint8_t i=0; i<size; ++i) pDataRd[i] = (uint8_t)Wire.read(); return true; } static uint32_t ArduinoMillis() { return millis(); } static void ArduinoDelay(uint32_t ms) { delay(ms); } // Helper to quickly set up a handle for Arduino Wire static bool MbI2c_BeginArduino(MbI2c_Handle_t* pHandle, uint8_t i2cAddress) { MbI2c_Init_t init; init.i2cAddress = i2cAddress; init.cbWrite = ArduinoI2C_Write; init.cbRead = ArduinoI2C_Read; init.cbGetMillis = ArduinoMillis; init.cbDelay = ArduinoDelay; return mbI2c_init(&init, pHandle); } // -------- Portable typed-read helpers (endianness & word-order control) ----- static inline uint16_t bswap16(uint16_t v) { return (uint16_t)((v >> 8) | (v << 8)); } static inline void swap16(uint16_t& a, uint16_t& b) { uint16_t t=a; a=b; b=t; } /* * Many metering ICs return: * - 16-bit words big-endian (byte order inside each 16-bit is MSB first), * - and a 32-bit value as two 16-bit words that may be [HiWord, LoWord] or [LoWord, HiWord]. * * Use these helpers to normalize into native CPU ordering for float/uint32. */ static bool mbRead2Words(MbI2c_Handle_t* h, uint16_t reg, MbI2c_RegType_e type, bool swapBytesEachWord, bool swapWordOrder, uint16_t outWords[2]) { uint16_t tmp[2] = {0,0}; bool ok = (type == MbI2c_RegType_Holding) ? mbI2c_readHolding(h, reg, tmp, 2) : mbI2c_readInput(h, reg, tmp, 2); if (!ok) return false; // optional byte swap within each 16-bit if (swapBytesEachWord) { tmp[0] = bswap16(tmp[0]); tmp[1] = bswap16(tmp[1]); } // optional word swap for 32-bit values if (swapWordOrder) swap16(tmp[0], tmp[1]); outWords[0] = tmp[0]; outWords[1] = tmp[1]; return true; } static bool mbReadFloatPortable(MbI2c_Handle_t* h, uint16_t reg, MbI2c_RegType_e type, bool swapBytesEachWord, bool swapWordOrder, float* out) { uint16_t w[2]; if (!mbRead2Words(h, reg, type, swapBytesEachWord, swapWordOrder, w)) return false; memcpy(out, w, sizeof(w)); // now in native layout return true; } static bool mbReadUint32Portable(MbI2c_Handle_t* h, uint16_t reg, MbI2c_RegType_e type, bool swapBytesEachWord, bool swapWordOrder, uint32_t* out) { uint16_t w[2]; if (!mbRead2Words(h, reg, type, swapBytesEachWord, swapWordOrder, w)) return false; memcpy(out, w, sizeof(w)); return true; } static bool mbReadUint16Portable(MbI2c_Handle_t* h, uint16_t reg, MbI2c_RegType_e type, bool swapBytesWord, uint16_t* out) { uint16_t v = 0; if (!( (type == MbI2c_RegType_Holding) ? mbI2c_readHolding(h, reg, &v, 1) : mbI2c_readInput(h, reg, &v, 1))) return false; if (swapBytesWord) v = bswap16(v); *out = v; return true; } |
Code/Program: IoT DC Energy Meter with ESP32 WebServer
This following code turns the ESP32 into an IoT smart DC energy meter that reads data from the DC Power Monitor Module over I²C. The ESP32 collects live measurements of voltage, current, power, and cumulative energy from Channel 1, processes the raw register values, and converts them into human-readable units. Sampling is done multiple times per second to ensure that the displayed values remain accurate and up to date.
At the same time, the ESP32 also runs as a Wi-Fi web server, hosting a modern, light-themed dashboard. This webpage, built with HTML, CSS, and JavaScript, uses AJAX to fetch new data every second without reloading. It shows the values in elegant cards with real-time sparklines, smooth animations, and a status bar for Wi-Fi and update timing. Anyone connected to the same Wi-Fi network can simply open the ESP32’s IP address in a browser to view live voltage, current, power, and energy readings in an attractive, dynamic interface.
From these lines change the WiFi SSID and password and replace with your network credentials.
|
1 2 3 |
// ===================== USER CONFIG ===================== #define WIFI_SSID "********************" #define WIFI_PASS "********************" |
After replacing the WiFi SSID and password, now you can upload the following code.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
/* * ESP32 DC Energy Meter - AJAX Web UI (Light Theme) * - Reads CH1 (V/I/W/Wh) via I2C * - Hosts a WiFi web server serving a polished light-themed page * - Page auto-refreshes values via AJAX (fetch /api) every second * - Minimal sparklines for each metric */ #include <WiFi.h> #include <WebServer.h> #include <Wire.h> #include "MbI2C_ArduinoPort.h" // ===================== USER CONFIG ===================== #define WIFI_SSID "********************" #define WIFI_PASS "********************" // I2C meter address static const uint8_t I2C_ADDR = 0x08; // 7-bit default // CH1 Input register addresses (decimal) static const uint16_t REG_VOLTAGE = 80; // U32 -> V static const uint16_t REG_CURRENT = 82; // U32 -> A static const uint16_t REG_POWER = 84; // U32 -> W static const uint16_t REG_ENERGY = 88; // U32 -> Wh // All are Input registers static const MbI2c_RegType_e VOLTAGE_TYPE = MbI2c_RegType_Input; static const MbI2c_RegType_e CURRENT_TYPE = MbI2c_RegType_Input; static const MbI2c_RegType_e POWER_TYPE = MbI2c_RegType_Input; static const MbI2c_RegType_e ENERGY_TYPE = MbI2c_RegType_Input; // Endianness (confirmed working for your board) static const bool BYTES_BIG_ENDIAN_IN_WORDS = false; static const bool SWAP_WORDS_32BIT = false; // Scaling static const float VOLT_SCALE = 0.001f; static const float CURR_SCALE = 0.001f; static const float POWR_SCALE = 0.001f; static const float ENER_SCALE = 0.001f; // Sampling & API cadence static const uint32_t SAMPLE_MS = 500; static const uint32_t API_CACHE_MS = 200; // ======================================================= MbI2c_Handle_t gMeter; WebServer server(80); // Last readings volatile float gV = 0, gA = 0, gW = 0, gE = 0; volatile bool gOkV = false, gOkA = false, gOkW = false, gOkE = false; uint32_t lastSampleMs = 0; // Build JSON cache String lastJson; uint32_t lastJsonMs = 0; // ------------ Meter helpers ------------ bool readU32Scaled(uint16_t reg, MbI2c_RegType_e type, float scale, float& outVal) { uint32_t raw = 0; if (!mbReadUint32Portable(&gMeter, reg, type, BYTES_BIG_ENDIAN_IN_WORDS, SWAP_WORDS_32BIT, &raw)) { return false; } outVal = raw * scale; return true; } void sampleMeterIfDue() { uint32_t now = millis(); if (now - lastSampleMs < SAMPLE_MS) return; lastSampleMs = now; float v=0, a=0, w=0, e=0; gOkV = readU32Scaled(REG_VOLTAGE, VOLTAGE_TYPE, VOLT_SCALE, v); gOkA = readU32Scaled(REG_CURRENT, CURRENT_TYPE, CURR_SCALE, a); gOkW = readU32Scaled(REG_POWER, POWER_TYPE, POWR_SCALE, w); gOkE = readU32Scaled(REG_ENERGY, ENERGY_TYPE, ENER_SCALE, e); if (gOkV) gV = v; if (gOkA) gA = a; if (gOkW) gW = w; if (gOkE) gE = e; } // ------------ Web content (Light theme) ------------ const char INDEX_HTML[] PROGMEM = R"HTML( <!doctype html> <html lang="en"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width,initial-scale=1"/> <title>DC Energy Meter</title> <style> :root{ --bg1:#f7f9fc; --bg2:#eef2f7; --card:#ffffff; --border:#e6e9ef; --text:#1f2430; --muted:#6b7280; --accent:#0ea5e9; --accent-soft:#dff3fc; --ok:#16a34a; --warn:#ca8a04; --shadow: 0 12px 30px rgba(14, 53, 108, 0.10); } *{box-sizing:border-box} html,body{height:100%} body{ margin:0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; color:var(--text); background: radial-gradient(1200px 900px at 20% 10%, var(--bg2) 0%, var(--bg1) 60%); display:flex; align-items:center; justify-content:center; padding:22px; } .wrap{ width:min(1100px, 96vw); display:flex; flex-direction:column; gap:18px; } .header{ display:flex; align-items:center; justify-content:space-between; gap:16px; } h1{ margin:0; font-weight:800; letter-spacing:0.2px; font-size: clamp(20px, 2.5vw, 26px); display:flex; gap:10px; align-items:center; } .brand{ display:inline-flex; width:30px; height:30px; border-radius:10px; background:linear-gradient(145deg,#e8f6ff,#ffffff); border:1px solid var(--border); box-shadow: var(--shadow); align-items:center; justify-content:center; color:var(--accent); font-weight:900;} .pill{ border:1px solid var(--border); background:#ffffffaa; backdrop-filter:blur(6px); color:var(--muted); padding:8px 12px; border-radius:999px; font-size:12px; } .grid{ display:grid; gap:16px; grid-template-columns:repeat(4,1fr); } @media (max-width: 900px){ .grid{ grid-template-columns:repeat(2,1fr);} } @media (max-width: 520px){ .grid{ grid-template-columns:1fr; } } .card{ position:relative; background:var(--card); border:1px solid var(--border); border-radius:18px; padding:16px 14px 12px 14px; box-shadow:var(--shadow); } .label{ font-size:12px; color:var(--muted); letter-spacing:.6px; text-transform:uppercase; display:flex; align-items:center; gap:8px; } .chip{ padding:4px 8px; border-radius:999px; font-size:11px; background:var(--accent-soft); color:#075985; } .value{ margin-top:6px; font-weight:800; font-size: clamp(24px, 4.5vw, 40px); line-height:1.1; letter-spacing:.3px; } .unit{ font-weight:700; font-size: clamp(12px,2vw,14px); color:var(--muted); margin-left:6px; } canvas{ width:100%; height:28px; display:block; margin-top:8px; } .footer{ display:flex; gap:10px; align-items:center; justify-content:flex-end; color:var(--muted); font-size:12px; } .badge{ color:var(--accent); font-weight:600; } .ok{ color:var(--ok); } .warn{ color:var(--warn); } /* subtle value animation */ .value{ transition: transform .25s ease, text-shadow .25s ease; } .value.bump{ transform: translateY(-2px) scale(1.01); text-shadow: 0 2px 14px rgba(14,165,233,0.25); } </style> </head> <body> <div class="wrap"> <div class="header"> <h1><span class="brand">⚡</span>DC Energy Meter</h1> <div class="pill"><span id="net">Connecting…</span></div> </div> <div class="grid"> <div class="card"> <div class="label">Voltage <span class="chip">Real-time</span></div> <div class="value" id="v">--.--<span class="unit">V</span></div> <canvas id="cv"></canvas> </div> <div class="card"> <div class="label">Current <span class="chip">Real-time</span></div> <div class="value" id="i">--.--<span class="unit">A</span></div> <canvas id="ci"></canvas> </div> <div class="card"> <div class="label">Power <span class="chip">Real-time</span></div> <div class="value" id="p">--.--<span class="unit">W</span></div> <canvas id="cp"></canvas> </div> <div class="card"> <div class="label">Energy <span class="chip">Cumulative</span></div> <div class="value" id="e">--.--<span class="unit">Wh</span></div> <canvas id="ce"></canvas> </div> </div> <div class="footer"> <span>Status:</span><span id="status" class="badge">starting…</span> <span>| Updated:</span><span id="ts">--:--:--</span> </div> </div> <script> const $ = s=>document.querySelector(s); const ids = { v: $("#v"), i: $("#i"), p: $("#p"), e: $("#e") }; const net = $("#net"), statusEl = $("#status"), ts = $("#ts"); // sparkline buffers const N = 60; const buf = { v: Array(N).fill(0), i: Array(N).fill(0), p: Array(N).fill(0), e: Array(N).fill(0), }; const cvs = { v: $("#cv"), i: $("#ci"), p: $("#cp"), e: $("#ce") }; // Resize canvases to device pixels function fitCanvas(c){ const r = window.devicePixelRatio || 1; const {width, height} = c.getBoundingClientRect(); c.width = Math.max(120, Math.floor(width * r)); c.height= Math.max(24, Math.floor(height* r)); } Object.values(cvs).forEach(fitCanvas); window.addEventListener('resize', ()=>Object.values(cvs).forEach(fitCanvas)); function stamp(){ const d=new Date(); const z=n=>String(n).padStart(2,'0'); ts.textContent=`${z(d.getHours())}:${z(d.getMinutes())}:${z(d.getSeconds())}`; } function bump(el){ el.classList.add('bump'); setTimeout(()=>el.classList.remove('bump'), 220); } function drawSpark(c, data, accent="#0ea5e9"){ const ctx = c.getContext('2d'); const W = c.width, H = c.height, pad=6; ctx.clearRect(0,0,W,H); const min = Math.min(...data); const max = Math.max(...data); const lo = min === max ? min-1 : min; const hi = min === max ? max+1 : max; const scale = v => H - pad - ( (v-lo) * (H-2*pad) / (hi-lo) ); // baseline ctx.strokeStyle = "#e6e9ef"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, H-pad); ctx.lineTo(W, H-pad); ctx.stroke(); // line ctx.strokeStyle = accent; ctx.lineWidth = 2; ctx.beginPath(); const step = W / (data.length-1); for(let i=0;i<data.length;i++){ const x = i*step, y = scale(data[i]); i?ctx.lineTo(x,y):ctx.moveTo(x,y); } ctx.stroke(); // fill fade const g = ctx.createLinearGradient(0,0,0,H); g.addColorStop(0, accent+"88"); g.addColorStop(1, accent+"00"); ctx.fillStyle = g; ctx.lineTo(W, H-pad); ctx.lineTo(0, H-pad); ctx.closePath(); ctx.fill(); } function push(buf, v){ buf.push(v); if(buf.length>N) buf.shift(); } async function fetchAPI(){ try{ const r = await fetch('/api',{cache:'no-store'}); if(!r.ok) throw new Error(r.statusText); const j = await r.json(); const v = +(j.v ?? 0), i = +(j.i ?? 0), p = +(j.p ?? 0), e = +(j.e ?? 0); // numbers ids.v.firstChild.nodeValue = v.toFixed(3)+' '; ids.i.firstChild.nodeValue = i.toFixed(3)+' '; ids.p.firstChild.nodeValue = p.toFixed(3)+' '; ids.e.firstChild.nodeValue = e.toFixed(3)+' '; bump(ids.v); bump(ids.i); bump(ids.p); bump(ids.e); // status const allOk = j.okV && j.okA && j.okW && j.okE; statusEl.textContent = allOk ? 'OK' : 'Partial data'; statusEl.className = allOk ? 'badge ok' : 'badge warn'; stamp(); // sparklines push(buf.v, v); push(buf.i, i); push(buf.p, p); push(buf.e, e); drawSpark(cvs.v, buf.v, "#0ea5e9"); drawSpark(cvs.i, buf.i, "#06b6d4"); drawSpark(cvs.p, buf.p, "#22c55e"); drawSpark(cvs.e, buf.e, "#a855f7"); }catch(e){ statusEl.textContent='Fetch error'; statusEl.className='badge warn'; } } async function fetchNet(){ try{ const r = await fetch('/net',{cache:'no-store'}); if(!r.ok) throw new Error(r.statusText); const j = await r.json(); net.textContent = j.ip ? `🟢 ${j.ssid} @ ${j.ip}` : 'WiFi?'; }catch(e){ net.textContent = 'WiFi?'; } } fetchNet(); fetchAPI(); setInterval(fetchAPI, 1000); setInterval(fetchNet, 5000); </script> </body> </html> )HTML"; // Handlers void handleRoot() { server.send_P(200, "text/html; charset=utf-8", INDEX_HTML); } void handleApi() { uint32_t now = millis(); if (now - lastJsonMs > API_CACHE_MS) { float v = gV, a = gA, w = gW, e = gE; bool okV = gOkV, okA = gOkA, okW = gOkW, okE = gOkE; String s; s.reserve(128); s += F("{\"v\":"); s += String(v,6); s += F(",\"i\":"); s += String(a,6); s += F(",\"p\":"); s += String(w,6); s += F(",\"e\":"); s += String(e,6); s += F(",\"okV\":"); s += (okV?"true":"false"); s += F(",\"okA\":"); s += (okA?"true":"false"); s += F(",\"okW\":"); s += (okW?"true":"false"); s += F(",\"okE\":"); s += (okE?"true":"false"); s += F("}"); lastJson = s; lastJsonMs = now; } server.send(200, "application/json; charset=utf-8", lastJson); } void handleNet() { String s; s.reserve(96); s += F("{\"ssid\":\""); s += WiFi.SSID(); s += F("\",\"ip\":\""); s += WiFi.localIP().toString(); s += F("\"}"); server.send(200, "application/json; charset=utf-8", s); } void handleNotFound() { server.send(404, "text/plain", "Not found"); } // ------------- Setup ------------- void setup() { Serial.begin(115200); delay(100); Serial.println("\nESP32 DC Energy Meter WebServer (Light Theme)"); // I2C / Meter Wire.begin(); // default pins (GPIO 21 SDA, 22 SCL) on many ESP32 dev boards Wire.setClock(400000); if (!MbI2c_BeginArduino(&gMeter, I2C_ADDR)) { Serial.println(F("Meter init failed!")); } // WiFi WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); Serial.printf("Connecting to WiFi: %s\n", WIFI_SSID); uint32_t t0 = millis(); while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000) { delay(250); Serial.print("."); } Serial.println(); if (WiFi.status() == WL_CONNECTED) { Serial.print("WiFi OK, IP: "); Serial.println(WiFi.localIP()); } else { Serial.println("WiFi failed; still serving (if connected later)."); } // HTTP routes server.on("/", handleRoot); server.on("/api", handleApi); server.on("/net", handleNet); server.onNotFound(handleNotFound); server.begin(); Serial.println("HTTP server started."); } // ------------- Loop ------------- void loop() { sampleMeterIfDue(); server.handleClient(); } |
Testing the IoT DC Energy Meter
In the Arduino IDE, select your ESP32 development board from the Board Manager and choose the correct COM port. Click on the Upload button to flash the code onto the ESP32.
Once the upload is complete, open the Serial Monitor and set the baud rate to match the one defined in your sketch (e.g., 115200). The ESP32 will attempt to connect to your Wi-Fi network, and you should see the connection status and the assigned IP address printed in the Serial Monitor.
Make sure the DC Power Monitor Module is powered with a suitable DC supply such as a battery or a variable power supply. Also, connect a load to the selected channel—for example, an LED, a small DC motor, or any other DC appliance within the supported range.
After everything is connected, open a browser on any device that is on the same Wi-Fi network and type in the ESP32’s IP address shown in the Serial Monitor. This will open the live web dashboard, where you can see real-time values of voltage, current, power, and energy updating automatically.
At the same time, you can also access the same page on your mobile phone for convenient monitoring.
To test the system, try varying the input supply voltage or changing the load, and you’ll immediately observe the updates on the dashboard.
Along with the numeric values, the page also shows graphical sparklines below each reading, giving you a quick visual representation of how the parameters change over time.
This makes it easy to analyze your DC system at a glance, whether you’re on a computer or a mobile device.
















