Bridging AC Theory and Open Source Electronics Projects
When exploring open source electronics projects, few builds bridge fundamental AC circuit theory and practical embedded debugging as effectively as a True RMS power analyzer. While commercial plug-in meters like the Kill-A-Watt cost around $30, building your own bench-grade analyzer forces you to confront the raw physics of alternating current: phase shifts, non-linear loads, and the critical difference between apparent and real power.
This guide walks through building a 120V/240V AC True RMS power meter using an ESP32, a ZMPT101B voltage transformer, and an ACS724 Hall-effect current sensor. We will cover the underlying AC theory, provide a complete data-dense specification table, map the hardware, and deliver fully compilable firmware. More importantly, we will debug the exact ADC errors that brick 90% of first-time builds.
Estimated Cost: $18 - $24 (USD, 2026 pricing).
Time to Build: 2 hours (hardware) + 1 hour (calibration).
AC Power Theory: What We Are Actually Measuring
Before wiring a single jumper, you must understand why average-responding multimeters fail on modern circuits. Most cheap meters assume a perfect sine wave and simply multiply the peak voltage by 0.707 to get RMS. But modern loads—LED drivers, switching power supplies, and variable frequency motor drives—draw current in sharp, non-linear spikes. To measure real power, we must sample the instantaneous voltage and current simultaneously, multiply them, and integrate over a full cycle.
| Parameter | Formula / Definition | Unit | Sensor Mapping & ESP32 Constraints |
|---|---|---|---|
| True RMS Voltage | $\sqrt{\frac{1}{T} \int_0^T v(t)^2 dt}$ | Volts (V) | ZMPT101B output. Must sample at ≥1kHz to capture harmonics. |
| True RMS Current | $\sqrt{\frac{1}{T} \int_0^T i(t)^2 dt}$ | Amps (A) | ACS724 (20A variant). Sensitivity: 100mV/A. Biased to VCC/2. |
| Real Power (P) | $\frac{1}{T} \int_0^T v(t) \cdot i(t) dt$ | Watts (W) | Calculated in firmware. Requires simultaneous ADC reads. |
| Apparent Power (S) | $V_{rms} \times I_{rms}$ | Volt-Amps (VA) | Simple multiplication of the two RMS values above. |
| Power Factor (PF) | $P / S$ (or $\cos \theta$) | Dimensionless (0-1) | Reveals phase shift. <0.9 indicates heavy reactive/inductive load. |
Note: For authoritative definitions of these power metrics and harmonic distortion limits, refer to the IEEE 519 standard for power quality in electrical systems.
Hardware Bill of Materials and Pin Mapping
The most common mistake in open source electronics projects involving AC sensing is ignoring the ESP32's internal ADC architecture. The ESP32 has two ADCs (ADC1 and ADC2). ADC2 is hardcoded to the Wi-Fi subsystem. If you enable Wi-Fi to log your power data, any pin mapped to ADC2 will throw a hardware fault. Therefore, we strictly use ADC1 pins (GPIO 32-39).
Parts List (2026 Variants)
- MCU: ESP32-DevKitC V4 (WROOM-32E module) - ~$6.00
- Voltage Sensor: ZMPT101B AC Voltage Sensor Module (with onboard LM358 op-amp) - ~$3.50
- Current Sensor: ACS724LLCTR-20AB-T (3.3V optimized, 20A bidirectional) - ~$4.50
- Passives: 10kΩ trimpot (for fine-tuning voltage offset), 0.1μF decoupling capacitors.
Pin Mapping Table
| Component | Sensor Pin | ESP32 Pin | Notes |
|---|---|---|---|
| ZMPT101B | OUT | GPIO 32 (ADC1_CH4) | Must not exceed 3.2V peak. Adjust onboard pot. |
| ZMPT101B | VCC | 5V (VIN) | Op-amp requires 5V for clean rail-to-rail swing. |
| ACS724 | OUT | GPIO 33 (ADC1_CH5) | Use the 3.3V variant to avoid frying the ESP32. |
| Both | GND | GND | Common ground is mandatory for ADC reference. |
Firmware: Complete Compilable Code
The following C++ code targets the ESP32-DevKitC V4 using the Arduino framework. It samples both channels simultaneously at roughly 2kHz, calculates the DC offset dynamically, and computes True RMS and Real Power over a 20ms window (one full 50Hz cycle, or slightly more than one 60Hz cycle to ensure full coverage). It includes error handling for out-of-bounds ADC reads.
#include <Arduino.h>
#include <WiFi.h>
#include <math.h>
// --- PIN DEFINITIONS (STRICTLY ADC1 TO AVOID WIFI CONFLICT) ---
#define PIN_V_SENSOR 32 // ADC1_CH4
#define PIN_I_SENSOR 33 // ADC1_CH5
// --- CALIBRATION CONSTANTS ---
// Adjust these based on your multimeter readings
#define V_CALIBRATION 0.185 // Volts per ADC step (depends on ZMPT101B pot)
#define I_CALIBRATION 0.012 // Amps per ADC step (ACS724 20A variant)
#define ADC_OFFSET 1890 // Theoretical 1.65V offset at 12-bit resolution (3300/2)
// --- TIMING ---
#define SAMPLE_WINDOW_MS 20 // 20ms covers one 50Hz cycle or 1.2x 60Hz cycle
#define SAMPLE_DELAY_US 450 // ~2.2kHz sampling rate
void setup() {
Serial.begin(115200);
delay(1000);
// Configure ADC1 pins for 12-bit resolution and 11dB attenuation (0-3.3V range)
analogReadResolution(12);
analogSetAttenuation(ADC_11db);
// Read initial offset to calibrate out any minor VCC/2 deviations
long v_sum = 0, i_sum = 0;
for(int i=0; i<100; i++) {
v_sum += analogRead(PIN_V_SENSOR);
i_sum += analogRead(PIN_I_SENSOR);
delayMicroseconds(100);
}
Serial.printf("Calibrated V_Offset: %d, I_Offset: %d\n", v_sum/100, i_sum/100);
Serial.println("ESP32 AC Power Analyzer Booted. Starting Wi-Fi...");
WiFi.begin("YourSSID", "YourPassword");
}
void loop() {
unsigned long start_time = millis();
double sum_v_sq = 0;
double sum_i_sq = 0;
double sum_p = 0;
int sample_count = 0;
int v_offset = analogRead(PIN_V_SENSOR); // Quick re-read for drifting offset
int i_offset = analogRead(PIN_I_SENSOR);
// In a production build, use a low-pass filter for the offset.
// Here we assume no load during boot for baseline.
while(millis() - start_time < SAMPLE_WINDOW_MS) {
int raw_v = analogRead(PIN_V_SENSOR);
int raw_i = analogRead(PIN_I_SENSOR);
// Error Handling: Check for ADC saturation or disconnected pins
if(raw_v > 4000 || raw_v < 50 || raw_i > 4000 || raw_i < 50) {
Serial.println("ERROR: ADC Saturation or Floating Pin Detected. Check wiring.");
delay(1000);
return;
}
double v_inst = (raw_v - v_offset) * V_CALIBRATION;
double i_inst = (raw_i - i_offset) * I_CALIBRATION;
sum_v_sq += v_inst * v_inst;
sum_i_sq += i_inst * i_inst;
sum_p += (v_inst * i_inst);
sample_count++;
delayMicroseconds(SAMPLE_DELAY_US);
}
if(sample_count > 0) {
double v_rms = sqrt(sum_v_sq / sample_count);
double i_rms = sqrt(sum_i_sq / sample_count);
double real_power = sum_p / sample_count;
double apparent_power = v_rms * i_rms;
double power_factor = (apparent_power > 0) ? abs(real_power / apparent_power) : 0;
Serial.printf("Vrms: %6.2f V | Irms: %5.3f A | Real: %6.2f W | PF: %.3f\n",
v_rms, i_rms, real_power, power_factor);
}
delay(500); // Pause before next reading cycle
}
Debugging: Ranked Causes for Common Build Failures
When transitioning from theory to the workbench, things break. If you are using Wi-Fi to push data to an MQTT broker or HTTP endpoint, you will almost certainly encounter the following fatal error in your serial monitor:
E (142) adc_common: adc2_get_raw(282): adc2 is in use by Wi-Fi
This is the most infamous trap in ESP32 open source electronics projects. Here are the ranked causes for ADC and measurement failures, starting with the most likely:
- ADC2 Pin Conflict (The Wi-Fi Trap): You wired your sensor to GPIO 25, 26, 27, 14, 12, 13, 15, 2, 0, or 4. The moment
WiFi.begin()executes, the ESP32's RF subsystem hijacks ADC2. Fix: Move all analog sensors to GPIO 32, 33, 34, 35, 36, or 39 (ADC1). - The 5V-to-3.3V Op-Amp Trap: The ZMPT101B module requires 5V to power its LM358 op-amp. However, a 5V supply means the AC waveform swings from 0V to 5V. The ESP32 ADC maxes out at 3.3V. If you plug a 5V-swinging signal into GPIO 32, you will permanently damage the ESP32's silicon. Fix: Power the ZMPT101B with 5V, but use a multimeter to adjust the blue onboard trimpot while the AC is connected. Ensure the peak AC voltage at the OUT pin never exceeds 3.2V. Alternatively, build a 2-resistor voltage divider on the output.
- Floating Ground Reference: You forgot to connect the GND pin of the sensors to the ESP32 GND. The ADC measures voltage relative to the MCU's ground plane. Without a common ground, the ADC reads random thermal noise. Fix: Verify continuity between sensor GND and ESP32 GND with your multimeter in beep mode.
- Non-Simultaneous Sampling (Phase Shift Error): If you use
analogRead()sequentially without accounting for the ~10μs conversion delay, your voltage and current samples will be slightly out of phase. On highly inductive loads (like motors), this artificial phase shift will skew your Power Factor and Real Power calculations. Fix: For hobbyist accuracy, the sequential read in the provided code is acceptable. For lab-grade accuracy, use the ESP-IDF ADC Continuous/DMA driver to read both channels in hardware-triggered lockstep.
- Verify your pin assignments are strictly on ADC1 (GPIO 32-39).
- Put a multimeter on the ZMPT101B OUT pin and verify the peak AC swing is < 3.2V.
- Check that the sensor GND is tied directly to the ESP32 GND, not just the 5V USB ground.
Extending and Simplifying the Build
One of the greatest advantages of building open source electronics projects from scratch is the ability to scale the design to your exact needs.
How to Simplify (DC Electronic Load)
If you don't need AC True RMS and just want to measure DC power (e.g., for a solar panel or battery discharge tester), strip out the ZMPT101B. Replace the ACS724 with a simple INA219 I2C shunt monitor. The INA219 handles the ADC conversion and multiplication internally via I2C, eliminating the need for high-speed analog sampling and offset calibration. You can drop the sample window code entirely and just poll ina219.getBusVoltage_V() and ina219.getCurrent_mA() every 500ms.
How to Extend (3-Phase Industrial Monitoring)
To scale this up for a 3-phase motor or solar inverter, you cannot use a single ESP32. The ADC multiplexer switching time will introduce fatal phase-shift errors between Phase A, B, and C. Instead, use three synchronized microcontrollers (like the ESP32-S3 or Raspberry Pi Pico), or a dedicated multi-channel ADC IC like the ADS131E08 (8-channel, simultaneous sampling, SPI interface). You will also need to implement the Clarke and Park transforms in your firmware to convert the 3-phase AC vectors into a rotating DC reference frame for accurate torque and power calculations.
For more advanced open-source power monitoring architectures, explore the hardware schemas available on Hackaday's power meter archives and the OpenEnergyMonitor project documentation.






