Built-in project · Arduino Uno R3

Panel load monitor on an Arduino Uno

Four ACS712 sensors on four breaker circuits, with low-voltage stand-in loads: OK, WARNING over 80% and OVERLOAD over 100% on RGB LEDs, plus total demand and the balance between the two legs. A teaching model, never for a real panel. The whole build, an Arduino Uno and 22 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 60 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $54. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Sensors
  • School
  • How an LED lights and why it needs a resistor
  • Choosing and reading a resistor
  • Difficulty: Intermediate
  • Time: About 60 minutes
  • Board: Arduino Uno R3
  • Parts: 28

How it works

Four ACS712 sensors on four breaker circuits, with low-voltage stand-in loads: OK, WARNING over 80% and OVERLOAD over 100% on RGB LEDs, plus total demand and the balance between the two legs. A teaching model, never for a real panel.

Arduino Uno R3 runs the sketch below. The circuit around it: LED, blue, Resistor, 220 Ω, RGB LED, Current sensor, ACS712, Bench power supply, Resistor, 1 Ω. pinMode sets LED_PIN[i][c] as an output. digitalWrite drives LED_PIN[i][2] LOW. analogRead measures the voltage on ACS_PIN[i] as a number. Serial.begin opens the serial port at 115200 baud. millis reads the time since power-up, so the sketch can time things without stopping.

Parts list

28 parts, plus the jumper wires and 4 bench instruments. The parts retail for about $54; on Mokxi they're free, and so are the instruments.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Resistor, 220 Ω (1) pin 2Arduino Uno R3 pin SDA
LED, blue pin CArduino Uno R3 pin GND
Resistor, 220 Ω (2) pin 2Arduino Uno R3 pin GND
Resistor, 220 Ω (3) pin 2Arduino Uno R3 pin GND
Resistor, 220 Ω (4) pin 2Arduino Uno R3 pin GND
Resistor, 220 Ω (5) pin 2Arduino Uno R3 pin GND
RGB LED (4) pin BArduino Uno R3 pin 13
RGB LED (4) pin GArduino Uno R3 pin 12
RGB LED (4) pin RArduino Uno R3 pin 11
RGB LED (3) pin BArduino Uno R3 pin 10
RGB LED (3) pin GArduino Uno R3 pin 9
RGB LED (3) pin RArduino Uno R3 pin 8
RGB LED (2) pin BArduino Uno R3 pin 7
RGB LED (2) pin GArduino Uno R3 pin 6
RGB LED (2) pin RArduino Uno R3 pin 5
RGB LED (1) pin BArduino Uno R3 pin 4
RGB LED (1) pin GArduino Uno R3 pin 3
RGB LED (1) pin RArduino Uno R3 pin 2
Arduino Uno R3 pin GNDGround
Current sensor, ACS712 (1) pin OUTArduino Uno R3 pin A0
Current sensor, ACS712 (2) pin OUTArduino Uno R3 pin A1
Current sensor, ACS712 (3) pin OUTArduino Uno R3 pin A2
Current sensor, ACS712 (4) pin OUTArduino Uno R3 pin A3
Resistor, 220 Ω (1) pin 1LED, blue pin A
Resistor, 220 Ω (2) pin 1RGB LED (1) pin C
Resistor, 220 Ω (3) pin 1RGB LED (2) pin C
Resistor, 220 Ω (4) pin 1RGB LED (3) pin C
Resistor, 220 Ω (5) pin 1RGB LED (4) pin C
Resistor, 1 Ω (1) pin 1Current sensor, ACS712 (1) pin IP+
Current sensor, ACS712 (1) pin IP-Ground
Bench power supply (1) pin V-Ground
Current sensor, ACS712 (1) pin VCCSupply
Current sensor, ACS712 (1) pin GNDGround
Resistor, 1 Ω (1) pin 2Bench power supply (1) pin V+
Resistor, 1 Ω (2) pin 1Current sensor, ACS712 (2) pin IP+
Current sensor, ACS712 (2) pin IP-Ground
Bench power supply (2) pin V-Ground
Current sensor, ACS712 (2) pin VCCSupply
Current sensor, ACS712 (2) pin GNDGround
Resistor, 1 Ω (2) pin 2Bench power supply (2) pin V+
Resistor, 1 Ω (3) pin 1Current sensor, ACS712 (3) pin IP+
Current sensor, ACS712 (3) pin IP-Ground
Bench power supply (3) pin V-Ground
Current sensor, ACS712 (3) pin VCCSupply
Current sensor, ACS712 (3) pin GNDGround
Resistor, 1 Ω (3) pin 2Bench power supply (3) pin V+
Resistor, 1 Ω (4) pin 1Current sensor, ACS712 (4) pin IP+
Current sensor, ACS712 (4) pin IP-Ground
Bench power supply (4) pin V-Ground
Current sensor, ACS712 (4) pin VCCSupply
Current sensor, ACS712 (4) pin GNDGround
Resistor, 1 Ω (4) pin 2Bench power supply (4) pin V+

The note on the canvas

Word for word, as it sits beside the circuit in the editor.

Try it
1. Press Run. All four circuits read OK (green) and the serial monitor shows the demand against the 100 A main.
2. Each bench supply and 1 ohm load stands in for a branch circuit: 1 bench amp stands for 10 real amps. Raise circuit 1 to 1.3 V (13 A of its 15 A breaker): after 3 seconds ("3 hours") it turns yellow, WARNING.
3. Raise it above 1.5 V: after a second it turns red, OVERLOAD.
4. Raise circuits 1 and 3 and leave 2 and 4 low: the imbalance LED lights when the two legs differ by more than 10 A.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Panel load monitor: four ACS712 sensors on four branch circuits, each
// checked against its breaker, plus the total demand and the balance between
// the panel's two legs. A TEACHING MODEL with low-voltage stand-ins: it is not
// a listed meter, and it must never be wired into a real panel, a building, or
// anything that protects people or property. Real panel work belongs to a
// licensed electrician.
//
//   A0..A3  ACS712-05B OUT for circuits 1..4 (each VCC on 5 V, GND on GND)
//   IP+/IP- each sensor in series with its load (a 1 ohm resistor on a small
//           bench supply, standing in for the branch circuit's lamps and loads)
//   D2 D3 D4     circuit 1 RGB LED (R G B)     D5 D6 D7    circuit 2
//   D8 D9 D10    circuit 3                     D11 D12 D13 circuit 4
//   A4      imbalance LED (digital output, also the SDA pin of the top header)
//   Each RGB LED is common cathode, with one 220 ohm resistor to ground.
//
// SCALE: the ACS712-05B reads up to 5 A, a branch breaker is 15 or 20 A. So
// one stand-in amp stands for 10 real amps. A 15 A breaker is 1.5 A on the
// bench, a 20 A breaker is 2.0 A, and the 100 A main is 10 A. Every number the
// sketch prints is in real (scaled) amps. One ADC step on the 5 A board is
// 26 mA, which is 0.26 A after scaling, so the readings are only that good.
//
// THE RULES IT CHECKS
//   A breaker is loaded to 80% for a continuous load, one that runs 3 hours
//   or more (NEC counts such a load at 125%, the same rule seen from the
//   other side). So per circuit:
//     OK        at or below 80% of the breaker rating
//     WARNING   over 80% (12 A on a 15 A breaker, 16 A on a 20 A one)
//     OVERLOAD  over 100% of the rating
//   LED colors: green, yellow, red. A change only counts after it has lasted
//   (no flapping on a blip). Scaled times, 1 second here for 1 hour there:
//     WARN_HOLD_MS  3000   over 80% for "3 hours" before WARNING
//     OVER_HOLD_MS  1000   over 100% before OVERLOAD (a real breaker has a
//                          thermal delay of its own; this is a short hold)
//     CLEAR_HOLD_MS 1000   a better state must also last before it shows
//
// DEMAND AND BALANCE
//   Total demand is the plain sum of the four circuits against a 100 A main
//   (a stand-in, simplified: a real main carries each leg's current, and a
//   load calculation is an electrician's job under NEC Article 220).
//   Circuits 1 and 3 are on leg A, 2 and 4 on leg B, the way breakers
//   alternate down a panel. The sketch prints both legs and the difference,
//   and lights the imbalance LED when it is over IMBALANCE_A for 3 seconds.
//   The limit is a teaching value, not a code number.

const int ACS_PIN[4] = {A0, A1, A2, A3};
const int LED_PIN[4][3] = {{2, 3, 4}, {5, 6, 7}, {8, 9, 10}, {11, 12, 13}};
const int IMBALANCE_LED = A4;

const int BREAKER_A[4] = {15, 15, 20, 20};  // breaker ratings in real amps
const int MAIN_A = 100;                     // the main, in real amps
const int IMBALANCE_A = 10;                 // leg difference that warns, amps
const float SCALE = 10.0;                   // real amps per stand-in amp
const float MV_PER_A = 185.0;               // ACS712-05B sensitivity
const float ADC_MV = 5000.0 / 1023.0;       // one converter step in mV
const float MID = 511.5;                    // 2.5 V on the 10 bit converter
const int SAMPLES = 64;                     // readings averaged per circuit

const unsigned long WARN_HOLD_MS = 3000;
const unsigned long OVER_HOLD_MS = 1000;
const unsigned long CLEAR_HOLD_MS = 1000;
const unsigned long IMBALANCE_HOLD_MS = 3000;
const unsigned long REPORT_MS = 2000;

enum Level { OK, WARNING, OVERLOAD };
const char *const NAME[3] = {"OK", "WARNING", "OVERLOAD"};

int amps10[4];                // measured real amps, tenths
Level shown[4] = {OK, OK, OK, OK};
Level cand[4] = {OK, OK, OK, OK};
unsigned long since[4];
bool imbalanced = false;
bool imbalanceCand = false;
unsigned long imbalanceSince = 0;
unsigned long lastReport = 0;

// Print tenths of an amp as 12.3
void printTenths(int t) {
  if (t < 0) { Serial.print('-'); t = -t; }
  Serial.print(t / 10);
  Serial.print('.');
  Serial.print(t % 10);
}

// What the current alone says: the 80% line is "over", so 12.0 A is still OK.
Level classify(int t, int breaker) {
  if (t > breaker * 10) return OVERLOAD;
  if (t > breaker * 8) return WARNING;
  return OK;
}

int readTenths(int i) {
  long sum = 0;
  for (int s = 0; s < SAMPLES; s++) sum += analogRead(ACS_PIN[i]);
  float counts = (float)sum / SAMPLES - MID;
  float standIn = counts * ADC_MV / MV_PER_A;
  float real = standIn * SCALE;
  if (real < 0.2 && real > -0.2) real = 0;
  return (int)(real * 10.0 + (real < 0 ? -0.5 : 0.5));
}

void showLed(int i) {
  digitalWrite(LED_PIN[i][0], shown[i] != OK ? HIGH : LOW);
  digitalWrite(LED_PIN[i][1], shown[i] != OVERLOAD ? HIGH : LOW);
  digitalWrite(LED_PIN[i][2], LOW);
}

void say(int i) {
  Serial.print("C");
  Serial.print(i + 1);
  Serial.print(' ');
  Serial.print(NAME[shown[i]]);
  Serial.print(": ");
  printTenths(amps10[i]);
  Serial.print(" A of ");
  Serial.print(BREAKER_A[i]);
  Serial.print(" A breaker, ");
  Serial.print((long)amps10[i] * 10 / BREAKER_A[i]);
  Serial.println("%");
}

void setup() {
  Serial.begin(115200);
  for (int i = 0; i < 4; i++)
    for (int c = 0; c < 3; c++) pinMode(LED_PIN[i][c], OUTPUT);
  pinMode(IMBALANCE_LED, OUTPUT);
  Serial.println("Mokxi Arduino Uno: panel load monitor (teaching model, low-voltage stand-ins)");
  Serial.println("Scale: 1 bench amp = 10 real amps. Breakers 15 15 20 20 A, main 100 A.");
  for (int i = 0; i < 4; i++) showLed(i);
}

void loop() {
  unsigned long now = millis();

  for (int i = 0; i < 4; i++) {
    amps10[i] = readTenths(i);
    Level raw = classify(amps10[i], BREAKER_A[i]);
    if (raw != cand[i]) {
      cand[i] = raw;
      since[i] = now;
    } else if (cand[i] != shown[i]) {
      unsigned long hold = CLEAR_HOLD_MS;
      if (cand[i] > shown[i]) hold = (cand[i] == OVERLOAD) ? OVER_HOLD_MS : WARN_HOLD_MS;
      if (now - since[i] >= hold) {
        Level from = shown[i];
        shown[i] = cand[i];
        showLed(i);
        Serial.print("C");
        Serial.print(i + 1);
        Serial.print(' ');
        Serial.print(NAME[from]);
        Serial.print(" -> ");
        say(i);
      }
    }
  }

  int legA = amps10[0] + amps10[2];
  int legB = amps10[1] + amps10[3];
  int diff = legA > legB ? legA - legB : legB - legA;
  bool over = diff > IMBALANCE_A * 10;
  if (over != imbalanceCand) {
    imbalanceCand = over;
    imbalanceSince = now;
  } else if (imbalanceCand != imbalanced && now - imbalanceSince >= IMBALANCE_HOLD_MS) {
    imbalanced = imbalanceCand;
    digitalWrite(IMBALANCE_LED, imbalanced ? HIGH : LOW);
    Serial.print(imbalanced ? "IMBALANCE WARNING: leg A " : "Balance OK: leg A ");
    printTenths(legA);
    Serial.print(" A, leg B ");
    printTenths(legB);
    Serial.print(" A, difference ");
    printTenths(diff);
    Serial.println(" A");
  }

  if (now - lastReport >= REPORT_MS) {
    lastReport = now;
    int total = amps10[0] + amps10[1] + amps10[2] + amps10[3];
    Serial.print("Demand ");
    printTenths(total);
    Serial.print(" A, ");
    Serial.print((long)total * 10 / MAIN_A);
    Serial.print("% of ");
    Serial.print(MAIN_A);
    Serial.print(" A main | legA ");
    printTenths(legA);
    Serial.print(" legB ");
    printTenths(legB);
    Serial.print(" diff ");
    printTenths(diff);
    Serial.print(" | ");
    for (int i = 0; i < 4; i++) {
      Serial.print('C');
      Serial.print(i + 1);
      Serial.print('=');
      printTenths(amps10[i]);
      Serial.print(' ');
      Serial.print(NAME[shown[i]]);
      Serial.print(' ');
    }
    Serial.println();
  }
}

Code walkthrough

The key functions of the sketch, and what each one does.

setup sketch.ino

Runs once when the board starts. It gets the pins and libraries ready.

void setup() {
  Serial.begin(115200);
  for (int i = 0; i < 4; i++)
    for (int c = 0; c < 3; c++) pinMode(LED_PIN[i][c], OUTPUT);
  pinMode(IMBALANCE_LED, OUTPUT);
  Serial.println("Mokxi Arduino Uno: panel load monitor (teaching model, low-voltage stand-ins)");
  Serial.println("Scale: 1 bench amp = 10 real amps. Breakers 15 15 20 20 A, main 100 A.");
  for (int i = 0; i < 4; i++) showLed(i);
}

loop sketch.ino

Runs over and over for as long as the board has power. This is where the project does its work.

void loop() {
  unsigned long now = millis();

  for (int i = 0; i < 4; i++) {
    amps10[i] = readTenths(i);
    Level raw = classify(amps10[i], BREAKER_A[i]);
    if (raw != cand[i]) {
      cand[i] = raw;
      since[i] = now;
    } else if (cand[i] != shown[i]) {
      unsigned long hold = CLEAR_HOLD_MS;
      if (cand[i] > shown[i]) hold = (cand[i] == OVERLOAD) ? OVER_HOLD_MS : WARN_HOLD_MS;
      if (now - since[i] >= hold) {
        Level from = shown[i];
        shown[i] = cand[i];
        showLed(i);
        Serial.print("C");
        Serial.print(i + 1);
        Serial.print(' ');
        Serial.print(NAME[from]);
        Serial.print(" -> ");
        say(i);
      }
    }
  }

  int legA = amps10[0] + amps10[2];
  int legB = amps10[1] + amps10[3];
  // ... 45 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The LED does not light, or it lights once and then dies. LED
    LEDs have polarity. The longer leg (anode) goes toward the positive side. Always add a series resistor, about 220 ohms to 1 k ohm on a 5 V pin.
  • A blue or white LED stays dark on 3.3 V. LED
    Blue and white LEDs drop about 3 V, so 3.3 V leaves almost nothing for the resistor. Use a 5 V supply, or a smaller resistor and a red or green LED.
  • The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
    Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure.
  • A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
    Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground.
  • The colors are wrong or one color never lights. RGB LED
    Check whether your LED is common cathode (shared leg to ground) or common anode (shared leg to 5 V). Common anode works backwards: LOW turns a color on.
  • One color is much brighter than the others, or the LED runs too hot. RGB LED
    Use one resistor per color leg, never one for the shared leg. Typical values are 220 ohm for red and 100 to 150 ohm for green and blue on 5 V.
  • The reading does not sit at zero amps. The output is about half of VCC at no current and moves only 185, 100 or 66 mV per amp, depending on the 5A, 20A or 30A part. Current sensor
    Read the idle value at startup and subtract it. Pick the version that fits your current and use the matching mV per amp. Average many samples to cut the noise.
  • The load is wired in parallel instead of in series, or a 3.3 V board reads wrong. The sensor needs 5 V and its output can be above 3.3 V. Current sensor
    Break the wire and run the load current through the IP+ and IP- terminals. Power the sensor from 5 V, and scale the output with a divider for 3.3 V ADCs.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.