Power Management & Switching Logic: Inside the Brawn Node
Saurab ThakurIn the first part of this series, we explored the high-level 4-node architecture of the V2.0 Dual-ESP32 UPS System. We established that the Brawn Node (ESP-WROOM-32) is the uncompromising, purely deterministic workhorse of the system.
But how does a tiny 3.3V microcontroller actually handle 220V AC mains and switch massive 12V LiFePO4 battery banks safely? In this article, we’ll dive deep into the electronic design, the physical isolation strategies, and the firmware loop that makes the Brawn node tick.
1. Safely Interfacing with Lethal Voltages
The number one rule when dealing with AC grid power (220V/110V) and a microcontroller is isolation. You absolutely cannot wire AC mains anywhere near the GPIO pins of an ESP32. A single voltage spike or improper ground will instantly incinerate the chip (and potentially you).
To solve this, we use Optocouplers (specifically, the PC817 4-Channel module).
Opto-Isolation 101
An optocoupler is essentially a tiny LED and a phototransistor sealed inside a tiny black plastic chip. On the “High Voltage” side of the circuit, we take a standard 5V USB wall adapter and plug it into the wall socket we want to monitor. The 5V DC output from this adapter powers the LED inside the optocoupler. On the “Low Voltage” side (connected to the ESP32), the phototransistor acts as a switch.
When grid power is ON:
- The wall adapter outputs 5V.
- The LED inside the PC817 shines.
- The phototransistor detects the light and closes the circuit.
- The ESP32’s GPIO pin is pulled LOW (or HIGH, depending on wiring).
When grid power fails, the light goes out, the circuit opens, and the ESP32 instantly detects the state change. At no point do the electrons from the wall ever touch the electrons on the ESP32 board. The transfer of information is purely optical.
In our UPS, we use three channels on the optocoupler board:
- Grid AC In: Detects if the neighborhood has power.
- UPS Inverter AC Out: Detects if our backup inverter has successfully engaged.
- P110 Smart Plug State: Detects the state of auxiliary power systems.
2. Reading Battery Chemistry: The Voltage Dividers
To know when to switch batteries, the system must precisely monitor the voltage of the 12V LiFePO4 packs. An ESP32’s Analog-to-Digital Converter (ADC) can only read voltages up to 3.3V. If you feed 12V directly into it, you will let the magic smoke out.
We use Voltage Dividers—a simple arrangement of two resistors in series. By using a 30kΩ resistor ($R_1$) and a 7.5kΩ resistor ($R_2$), we can step down a maximum of 25V input into a maximum of 3.3V output.
$$V_{out} = V_{in} \times \frac{R_2}{R_1 + R_2}$$
The stepped-down voltage is fed into GPIO 34 and GPIO 35. However, the ESP32’s internal ADCs are notoriously non-linear and noisy. To combat this in firmware, the Brawn node takes multisampled readings:
int getSmoothedADC(int pin) {
int total = 0;
for(int i = 0; i < 20; i++) {
total += analogRead(pin);
delay(2);
}
return total / 20;
}
By averaging 20 rapid readings, we smooth out electrical noise. We then apply a software calibration multiplier (e.g., * 0.00543) to convert the raw 0-4095 ADC value back into a highly accurate true voltage.
3. The Art of Relay Switching
When it’s time to actually cut power or swap battery banks, the Brawn node uses a small army of relays. But not all relays are created equal. We utilize three distinct types of relays to maximize efficiency and longevity.
Solid State Relays (SSR - G3MB-202P)
Mechanical relays “click” loudly and their physical contacts wear out over thousands of cycles. For tasks that happen frequently—like pulsing a warning buzzer or toggling a low-current 14.6V LiFePO4 battery charger on and off—we use Solid State Relays. They use semiconductor switching, meaning they are completely silent, switch instantly, and never degrade mechanically.
The 1-Way Battery Swap Relay (SPDT)
The system has two battery packs: a smaller “Network Pack” (for the router/modem) and a massive “UPS Pack” (for the servers). We use a Single-Pole Double-Throw (SPDT) mechanical relay for the switchover.
This relay is wired for fail-safes:
- Normally Closed (NC): Connected to the Network Pack.
- Normally Open (NO): Connected to the UPS Pack.
If the Brawn node completely dies, crashes, or loses power, the relay’s coil de-energizes. It physically snaps back to the NC position. This means the system will always default to the Network Pack if things go wrong, ensuring the router stays online so I can remotely debug the issue!
Zero Power Waste: Latching Relays (TQ2-L2)
Standard relays require a constant flow of electricity (around 70mA to 100mA) through their electromagnet coil to stay activated. During a multi-day power outage, wasting precious battery juice just to hold a relay open is unacceptable.
Enter the Dual-Coil Latching Relay. This mechanical relay contains a tiny permanent magnet. It requires a short 50-millisecond pulse of electricity to switch states (SET), and another 50ms pulse on a different pin to switch back (RESET). Once it flips, it magnetically locks into place and consumes zero power.
The Brawn node uses BC547 NPN transistors to drive these pulses. When the batteries are critically depleted and it’s time to completely cut power to save the LiFePO4 cells from deep-discharge damage, the Brawn node fires the RESET pulse. The relay clicks off, and the entire system draws 0.00 Amps.
4. The Deterministic Firmware Loop
Because the Brawn node doesn’t have Wi-Fi enabled, the Arduino loop() function executes blazingly fast. It guarantees real-time responsiveness.
Here is the pseudo-code architecture of the Brawn node:
void loop() {
// 1. Read the environment (Non-blocking)
float netBatteryVolt = readAndAverage(34);
float upsBatteryVolt = readAndAverage(35);
bool isGridOnline = digitalRead(OPTO_GRID_PIN);
// 2. Read physical override buttons (with debouncing)
checkHardwareButtons();
// 3. Process instructions from the Brain (via Serial RX)
if (Serial2.available()) {
String payload = Serial2.readStringUntil('\n');
executeBrainCommand(payload);
}
// 4. Blast state back to the Brain (via Serial TX)
if (millis() - lastBroadcast > 500) {
sendTelemetryJSON();
lastBroadcast = millis();
}
}
Notice what isn’t there. There are no delay() statements. There are no WiFi.reconnect() loops. There are no HTTP clients timing out. It is a pure sense-and-react state machine.
By offloading the heavy lifting to the Brain node, the Brawn node achieves industrial-level reliability. If the local network crashes, the Brawn node doesn’t care. It just keeps measuring voltages and holding the relays steady.
In Part 3, we will shift our focus to the Brain Node (ESP32-S3)—the intelligent conductor that analyzes this telemetry, manages graceful server shutdowns over the network, and serves the beautiful Astro dashboard.
