Custom LiFePO4 battery pack with active balancing BMS and ESP32 thermal monitoring system

Figure 1: A professional-grade DIY LiFePO4 battery pack featuring active cell balancing and real-time thermal monitoring via ESP32.

⚠️ CRITICAL SAFETY WARNING: LITHIUM BATTERY FIRE HAZARD

  • Thermal Runaway Risk: Even LiFePO4 cells, while much safer than Li-Ion, can vent gases or catch fire if short-circuited, overcharged, or physically damaged. Work in a ventilated area away from flammable materials.
  • High Current Danger: A 100Ah LiFePO4 pack can deliver 500A+ in a short circuit. Always use insulated tools, remove jewelry, and work on a non-conductive surface.
  • BMS is Mandatory: Never operate a multi-cell LiFePO4 pack without a properly rated BMS. The BMS protects against overvoltage, undervoltage, overcurrent, and cell imbalance — all of which can lead to catastrophic failure.
  • Emergency Preparedness: Keep a Class D fire extinguisher or large bucket of sand nearby. Never use water on a lithium battery fire.

Welcome to TechFix Hub. You've decided to build your own LiFePO4 battery pack — a wise choice for 2026 with the explosive growth of EVs, home energy storage, and off-grid solar systems. But here's the hard truth: a pack without proper cell management is a ticking time bomb.

Most commercial BMS units use passive balancing, which wastes energy as heat and is painfully slow. In this comprehensive guide, we'll build a DIY LiFePO4 BMS with active balancing that transfers energy between cells (not wastes it), paired with an ESP32 thermal monitoring system that watches every cell's temperature 24/7 and alerts you before problems escalate. Whether you call it BMS équilibrage actif or active balancing BMS DIY, the result is the same: a safer, longer-lasting, smarter battery pack.

🔋 1. Why Active Balancing Changes Everything

Let's start with the fundamental question: why spend more on active balancing when passive balancing "works"?

Feature Passive Balancing (Standard BMS) Active Balancing (Pro BMS)
Method Bleeds excess charge as heat through resistors Transfers energy from strong cells to weak cells via DC-DC converters
Efficiency 0% (all energy wasted) 90-95% (energy preserved)
Balancing Current 30-100 mA (very slow) 1-5A (10-50x faster)
Heat Generation Significant (requires cooling) Minimal (cool operation)
Works During Discharge? No (only during charging) Yes (continuous balancing)
Pack Lifespan Impact Moderate improvement 20-40% longer cycle life

The bottom line: For packs under 50Ah, passive balancing is acceptable. For packs 100Ah and above (solar storage, EV conversions, marine), active balancing is not optional — it's essential for safety and longevity.

🛠️ 2. Bill of Materials: Building a Pro-Grade BMS

This BOM is designed for a 4S 100Ah 12V LiFePO4 pack (the most common DIY configuration). Scale accordingly for 8S/16S systems.

Component Specification Purpose Est. Cost (2026)
LiFePO4 Cells 4x 3.2V 100Ah Grade A prismatic (EVE/CATL/CALB) Energy storage ~$400
Active Balancer 4S 2A active balancer module (e.g., EEL or E-Fuel) Cell-to-cell energy transfer ~$80
Smart BMS 4S 100A BMS with UART/CAN + Bluetooth (JBD or Daly) Protection + monitoring ~$60
ESP32 Controller ESP32-WROOM-32 DevKit LiFePO4 ESP32 monitor brain ~$6
Thermal Sensors 4x DS18B20 waterproof temperature probes Per-cell surveillance batterie ESP32 ~$12
Current Sensor INA226 (with 50A shunt) or Victron SmartShunt Precise current measurement ~$15-50
Busbars Pure copper, 10mm x 30mm, tinned Low-resistance cell interconnects ~$25
Main Fuse Class T fuse 150A + holder Short-circuit protection ~$20
Power Cables 2 AWG silicone wire, 1m red + 1m black Main power connections ~$15
Enclosure ABS or aluminum battery box with ventilation Mechanical protection ~$30

Total estimated cost: ~$660 for a 12V 100Ah pack with full active balancing and ESP32 monitoring — roughly 40% less than equivalent commercial units, with 3x the intelligence.

🔧 3. Step-by-Step Assembly: From Cells to Smart Pack

Phase 1: Cell Preparation (The Most Critical Step)

  1. Top Balancing: Before assembly, charge each cell individually to exactly 3.65V using a lab power supply. Hold at 3.65V until current drops below 0.5A. This ensures all cells start at the same state of charge.
  2. Capacity Testing: Discharge each cell at 0.5C (50A for 100Ah cells) down to 2.5V using your DIY Electronic Load. Record actual capacity — cells within 5% of each other are acceptable. Reject cells with >10% variance.
  3. Internal Resistance Check: Use an AC impedance meter or battery analyzer. Cells should have internal resistance within 0.1mΩ of each other. High-IR cells will become weak links.

Phase 2: Mechanical Assembly

  1. Compression: LiFePO4 cells perform best under slight compression (~150kg force). Use threaded rods with end plates to apply even pressure across the cell stack.
  2. Busbar Torque: Tighten M6 terminal bolts to exactly 5-6 N·m using a torque wrench. Under-torque = hot spots = fire risk. Over-torque = stripped aluminum terminals.
  3. Series Connection: Connect cells in series: Cell 1 negative → Cell 2 positive → Cell 2 negative → Cell 3 positive → etc. Total nominal voltage: 4 × 3.2V = 12.8V.

Phase 3: BMS and Active Balancer Installation

  1. BMS Sense Wires: Connect balance leads in exact order: B- (pack negative) → C1 (between cell 1 & 2) → C2 → C3 → B+ (pack positive). Verify voltage at each wire before connecting to BMS. Wrong order = instant BMS destruction.
  2. Active Balancer Connection: Most active balancers connect in parallel with the BMS sense wires. They monitor cell voltages and automatically transfer energy from higher cells to lower cells during charge AND discharge.
  3. Main Power Path: BMS negative output → Class T fuse → main negative terminal. BMS positive connects directly to main positive terminal.

🌡️ 4. ESP32 Thermal Monitoring: Your 24/7 Battery Guardian

This is where the LiFePO4 ESP32 monitor truly shines. While the BMS handles protection, the ESP32 provides intelligence, logging, and remote alerts.

Sensor Placement Strategy

  • One DS18B20 per cell: Attach directly to the center of each cell's flat surface using thermal epoxy or Kapton tape. This catches hot spots before they propagate.
  • BMS temperature sensor: Most BMS units have built-in temp sensors. Monitor these too — they detect ambient pack temperature.
  • Busbar temperature (optional): Add a thermocouple to the hottest busbar connection to detect loose terminals before they fail.

The Complete ESP32 Monitoring Code

// TechFix Hub - LiFePO4 ESP32 Monitor with Active Balancing Status
// Full surveillance batterie ESP32 system with MQTT alerts

#include <WiFi.h>
#include <PubSubClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <HardwareSerial.h>

// WiFi & MQTT Configuration
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.100";  // Home Assistant IP
WiFiClient espClient;
PubSubClient mqtt(espClient);

// DS18B20 Temperature Sensors (OneWire bus on GPIO 4)
#define ONE_WIRE_BUS 4
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature tempSensors(&oneWire);
DeviceAddress tempCell1, tempCell2, tempCell3, tempCell4;

// BMS UART Communication (JBD protocol)
HardwareSerial BMS_Serial(2);  // UART2 on GPIO 16/17

// Safety Thresholds
const float TEMP_WARNING = 45.0;    // °C - Send alert
const float TEMP_CRITICAL = 55.0;   // °C - Disconnect loads
const float VOLT_CELL_MAX = 3.65;   // V - Per cell
const float VOLT_CELL_MIN = 2.80;   // V - Per cell
const float BALANCE_DELTA = 0.020;  // V - Active balancer trigger

// Timing
unsigned long lastTempRead = 0;
unsigned long lastBMSRead = 0;
unsigned long lastMQTTPublish = 0;

// Cell data structure
struct CellData {
  float voltage;
  float temperature;
  bool isBalancing;
} cells[4];

float packVoltage = 0;
float packCurrent = 0;
float packSOC = 0;

void setup() {
  Serial.begin(115200);
  Serial.println("🔋 TechFix Hub LiFePO4 Monitor Starting...");
  
  // Initialize temperature sensors
  tempSensors.begin();
  tempSensors.getAddress(tempCell1, 0);
  tempSensors.getAddress(tempCell2, 1);
  tempSensors.getAddress(tempCell3, 2);
  tempSensors.getAddress(tempCell4, 3);
  tempSensors.setResolution(12);  // High precision
  
  // Initialize BMS UART (9600 baud for JBD protocol)
  BMS_Serial.begin(9600, SERIAL_8N1, 16, 17);
  
  // Connect WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\n✅ WiFi Connected");
  
  // Connect MQTT
  mqtt.setServer(mqtt_server, 1883);
  mqtt.setCallback(mqttCallback);
  
  Serial.println("🛡️ Battery monitoring active. Safety thresholds loaded.");
}

void loop() {
  // Maintain MQTT connection
  if (!mqtt.connected()) {
    reconnectMQTT();
  }
  mqtt.loop();
  
  unsigned long now = millis();
  
  // Read temperatures every 2 seconds
  if (now - lastTempRead > 2000) {
    readTemperatures();
    lastTempRead = now;
  }
  
  // Read BMS data every 1 second
  if (now - lastBMSRead > 1000) {
    readBMSData();
    lastBMSRead = now;
  }
  
  // Publish to MQTT every 5 seconds
  if (now - lastMQTTPublish > 5000) {
    publishToMQTT();
    lastMQTTPublish = now;
  }
  
  // Safety checks (continuous)
  checkSafetyLimits();
}

void readTemperatures() {
  tempSensors.requestTemperatures();
  
  cells[0].temperature = tempSensors.getTempC(tempCell1);
  cells[1].temperature = tempSensors.getTempC(tempCell2);
  cells[2].temperature = tempSensors.getTempC(tempCell3);
  cells[3].temperature = tempSensors.getTempC(tempCell4);
  
  // Debug output
  for (int i = 0; i < 4; i++) {
    Serial.print("Cell ");
    Serial.print(i+1);
    Serial.print(" Temp: ");
    Serial.print(cells[i].temperature);
    Serial.println("°C");
  }
}

void readBMSData() {
  // Send read command to JBD BMS (0xDD 0xA5 0x03 0x00 0xFF 0xFD 0x77)
  byte readCmd[] = {0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77};
  BMS_Serial.write(readCmd, 7);
  
  // Wait for response
  if (BMS_Serial.available()) {
    byte response[40];
    int len = BMS_Serial.readBytes(response, 40);
    
    // Parse JBD protocol (simplified)
    if (len > 10 && response[0] == 0xDD && response[1] == 0x03) {
      packVoltage = (response[4] << 8 | response[5]) / 100.0;
      packCurrent = (response[6] << 8 | response[7]) / 100.0;
      packSOC = response[23];  // State of Charge %
      
      // Read individual cell voltages
      for (int i = 0; i < 4; i++) {
        cells[i].voltage = (response[8 + i*2] << 8 | response[9 + i*2]) / 1000.0;
      }
      
      // Check balancing status (bitmask in response)
      byte balanceStatus = response[12];
      for (int i = 0; i < 4; i++) {
        cells[i].isBalancing = (balanceStatus & (1 << i)) != 0;
      }
    }
  }
}

void publishToMQTT() {
  char topic[50], payload[20];
  
  // Pack-level data
  mqtt.publish("battery/pack/voltage", String(packVoltage, 2).c_str());
  mqtt.publish("battery/pack/current", String(packCurrent, 2).c_str());
  mqtt.publish("battery/pack/soc", String(packSOC).c_str());
  
  // Per-cell data
  for (int i = 0; i < 4; i++) {
    sprintf(topic, "battery/cell%d/voltage", i+1);
    mqtt.publish(topic, String(cells[i].voltage, 3).c_str());
    
    sprintf(topic, "battery/cell%d/temperature", i+1);
    mqtt.publish(topic, String(cells[i].temperature, 1).c_str());
    
    sprintf(topic, "battery/cell%d/balancing", i+1);
    mqtt.publish(topic, cells[i].isBalancing ? "1" : "0");
  }
  
  // Cell voltage delta (imbalance indicator)
  float maxV = cells[0].voltage, minV = cells[0].voltage;
  for (int i = 1; i < 4; i++) {
    if (cells[i].voltage > maxV) maxV = cells[i].voltage;
    if (cells[i].voltage < minV) minV = cells[i].voltage;
  }
  float delta = maxV - minV;
  mqtt.publish("battery/pack/cell_delta", String(delta, 3).c_str());
}

void checkSafetyLimits() {
  // Temperature checks
  for (int i = 0; i < 4; i++) {
    if (cells[i].temperature > TEMP_CRITICAL) {
      mqtt.publish("battery/alert/critical", 
        String("CRITICAL: Cell ") + (i+1) + " at " + cells[i].temperature + "°C!");
      // TODO: Trigger emergency contactor disconnect via GPIO
    } else if (cells[i].temperature > TEMP_WARNING) {
      mqtt.publish("battery/alert/warning",
        String("WARNING: Cell ") + (i+1) + " at " + cells[i].temperature + "°C");
    }
  }
  
  // Voltage checks
  for (int i = 0; i < 4; i++) {
    if (cells[i].voltage > VOLT_CELL_MAX || cells[i].voltage < VOLT_CELL_MIN) {
      mqtt.publish("battery/alert/voltage",
        String("ALERT: Cell ") + (i+1) + " voltage out of range: " + cells[i].voltage + "V");
    }
  }
  
  // Cell imbalance check (indicates balancer failure)
  float maxV = cells[0].voltage, minV = cells[0].voltage;
  for (int i = 1; i < 4; i++) {
    if (cells[i].voltage > maxV) maxV = cells[i].voltage;
    if (cells[i].voltage < minV) minV = cells[i].voltage;
  }
  if ((maxV - minV) > 0.100) {  // 100mV imbalance
    mqtt.publish("battery/alert/imbalance",
      String("ALERT: Cell imbalance ") + (maxV - minV) + "V - check active balancer!");
  }
}

void reconnectMQTT() {
  while (!mqtt.connected()) {
    Serial.print("Connecting to MQTT...");
    String clientId = "ESP32-Battery-" + String(random(0xffff), HEX);
    if (mqtt.connect(clientId.c_str(), "mqtt_user", "mqtt_password")) {
      Serial.println("connected");
      mqtt.subscribe("battery/command/#");
    } else {
      Serial.print("failed, rc=");
      Serial.print(mqtt.state());
      Serial.println(" retry in 5s");
      delay(5000);
    }
  }
}

void mqttCallback(char* topic, byte* payload, unsigned int length) {
  // Handle remote commands (e.g., reset BMS, change thresholds)
  String cmd = String(topic);
  if (cmd == "battery/command/reset") {
    Serial.println("Remote reset requested");
    // TODO: Implement BMS reset via UART
  }
}
      

🏠 5. Home Assistant Integration: Visualizing Your Battery

With MQTT publishing every 5 seconds, Home Assistant can create a beautiful, actionable dashboard:

  • Cell Voltage Graph: Use the "ApexCharts Card" to plot all 4 cell voltages over time. Watch them converge during active balancing — it's deeply satisfying.
  • Temperature Heatmap: Create a visual grid showing each cell's temperature. Color-code cells: green (<35 red="" yellow="">45°C).
  • SOC Gauge: A circular gauge showing pack state of charge, with color transitions (green >50%, yellow 20-50%, red <20 li="">
  • Automation Examples:
    • "If cell_delta > 50mV for 1 hour → send Telegram alert: 'Active balancer may be failing'"
    • "If any cell temp > 50°C → turn off solar charger via MQTT command"
    • "If SOC < 15% AND time is off-peak → enable grid charger to protect battery"

🚀 6. The "TechFix Hub" Upgrade: Predictive Battery Health

Take your surveillance batterie ESP32 system from reactive to predictive with these advanced features:

  • SOH (State of Health) Estimation: Once a month, trigger a controlled discharge test using your DIY Electronic Load. Compare actual capacity vs. rated capacity. Log the degradation curve to predict remaining lifespan.
  • Thermal Camera Integration: Add an MLX90640 thermal camera (I2C) pointed at the cell stack. The ESP32 can detect hot spots invisible to point sensors, catching failing connections days before they become dangerous.
  • Machine Learning Anomaly Detection: Use Edge Impulse to train a TinyML model on normal battery behavior. The ESP32 can detect subtle anomalies (unusual voltage sag, temperature spikes) that indicate impending failure.
  • Redundant Safety Contactor: Add a high-current relay (e.g., Gigavac GV200) controlled by the ESP32. If the BMS fails to disconnect during a fault, the ESP32 acts as a backup safety system.

🔧 7. Troubleshooting Common Issues

  • Cells won't balance despite active balancer: Check that the balancer's current rating matches your cell capacity. A 2A balancer on 280Ah cells will take DAYS to balance. Consider upgrading to a 5A+ balancer or adding multiple units in parallel.
  • ESP32 keeps rebooting: The DS18B20 sensors and BMS UART can draw enough current to cause brownout on weak USB power. Use a dedicated 5V 2A regulator for the ESP32, separate from the pack power.
  • Temperature readings erratic or -127°C: Check OneWire wiring. DS18B20 requires a 4.7kΩ pull-up resistor on the data line. Verify sensor addresses match physical placement.
  • BMS UART not responding: JBD BMS uses 9600 baud. Daly BMS uses 9600 but different protocol. Verify you're using the correct protocol parser. Some BMS units ship with UART disabled — enable via the manufacturer's Bluetooth app first.
  • One cell heats more than others: This indicates higher internal resistance in that cell. Check busbar torque on that cell's terminals. If torque is correct, the cell may be defective — isolate and test separately.

🔗 Complete Your Energy Storage System on TechFix Hub

This BMS pairs perfectly with our other power electronics guides:

✅ Conclusion

Building a DIY LiFePO4 BMS with active balancing and ESP32 thermal monitoring is the ultimate project for any serious energy storage enthusiast. You've moved beyond basic battery assembly into the realm of intelligent, self-aware power systems. The active balancer ensures your cells age evenly, extending pack life by years. The ESP32 monitor provides real-time visibility into every cell's health, catching problems before they become emergencies. In 2026, with the rise of home energy storage and EV conversions, these skills are more valuable than ever. Build safe, build smart, and let your battery pack last a decade or more.

🛠️ Building your own LiFePO4 battery pack?

Questions about active balancer selection, DS18B20 placement, or ESP32 MQTT integration? Drop your questions in the comments below, and the TechFix Hub community will help you build a safe, intelligent battery system!

Keywords: Active balancing BMS DIY LiFePO4 ESP32 monitor BMS équilibrage actif Surveillance batterie ESP32 LiFePO4 Battery Pack

Bottom Ad [Post Page]