Community project

Battery Health Screening Device

ESP32
Photo of Battery Health Screening Device

TheDum -E

Published October 9, 2026

This battery health screening device measures the capacity and condition of single-cell 18650 lithium batteries by applying a controlled load and monitoring voltage, current, and temperature over a 30-second test cycle. The ESP32 controller manages a MOSFET-switched load path, records real-time data to an SD card, and displays results on an OLED screen with LED indicators and audio feedback.

Builders will receive a complete wiring diagram, parts list, and firmware that implements voltage divider measurement via the ADS1115 ADC, current sensing through the INA219 module, and NTC thermistor temperature compensation. The guide covers safe assembly of the battery fixture and load circuit, configuration of the test parameters, and interpretation of the pass/inspect/recycle outcomes.

Wiring diagram

Wiring diagram for Battery Health Screening Device

Gather all the parts

QtyComponent
1

ADS1115 16-Bit ADC Module

16-bit four-channel I2C analog-to-digital converter with programmable gain amplifier. Common ADS1115 breakouts expose MCU-facing SDA/SCL pins and four analog inputs for single-ended or differential measurements.

1

Adafruit INA219 High-Side DC Current Sensor

INA219 high-side current and bus-voltage monitor breakout. It is powered from 3.3V or 5V and communicates over I2C. Route the measured load current through VIN+ and VIN-; those shunt terminals are part of the power path, not MCU GPIO.

1

SSD1306 OLED

0.96 inch 128x64 OLED display with I2C interface

1

MicroSD Card Module

SPI-based microSD card adapter module for SPI-capable microcontrollers. Uses MOSI, MISO, SCK, and CS plus power and ground. Many low-cost modules include a 3.3 V regulator and level shifting for 5 V MCU boards, while bare breakouts should be powered and signalled at 3.3 V.

1

IRLZ44N Logic-Level N-Channel MOSFET

Logic-level N-channel MOSFET commonly used as a low-side switch for DC loads from 3.3V or 5V microcontroller GPIO. Use a gate resistor, a gate pulldown, and a flyback diode for inductive loads.

1

10 kΩ NTC thermistor, two-lead

A two-lead temperature sensor taped to the 18650 cell to stop the test if the cell warms too much.

1

Buzzer

Piezo buzzer for sound output

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

LED

Green

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

Yellow

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

Red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

Resistor

100 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

10 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

4.7 Ω, 10 W wirewound

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

20 kΩ, 1%

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

10 kΩ, 1%

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

10 kΩ, 1%

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

220 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

220 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

220 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Single-cell 18650 insulated test fixture

An insulated holder and red/black test leads that hold one protected 18650 cell while it is measured.

1

1.5 A inline DC test-path fuse and holder

A replaceable fuse in the battery-positive test path that limits fault current from the cell.

1

Latching emergency-stop push button, normally closed contact

A red latching safety switch that physically disconnects the battery test path when pressed.

Assemble it in 7 steps

1. Keep the controller on USB power

Place the ESP32 across the breadboard center gap, then power it only from its USB socket while you build. Run one short black jumper from an ESP32 GND pin to the breadboard ground rail and one red jumper from 3V3 to a separate 3.3 V rail; do not connect the 18650 positive lead to either ESP32 power pin.

  • Use the USB cable both for power and later firmware deployment.
  • Never power the ESP32 from the cell being tested; a failing cell can pull the controller voltage down or damage modules.

2. Build the protected battery and load path

With no cell installed, connect the red battery-fixture lead to the emergency-stop COM terminal, the emergency-stop NC terminal to the fuse IN terminal, fuse OUT to INA219 VIN+, INA219 VIN- to one end of the 4.7 Ω 10 W resistor, and the other end of that resistor to the MOSFET DRAIN. Connect MOSFET SOURCE to the ground rail. This is the high-current path: cell positive → safety switch → fuse → current sensor → hot load resistor → MOSFET → cell negative.

  • Use the thick test leads for every wire from the cell through the resistor and MOSFET.
  • For a TO-220 IRLZ44N with the flat labelled face toward you and legs down, the usual leg order is Gate, Drain, Source; confirm against the marking or datasheet of your exact part.
  • Keep the 10 W resistor and MOSFET heatsink off the breadboard and away from plastic; the resistor becomes hot during a test.
  • Press the emergency stop before changing any test-path wiring or inserting/removing a cell.

3. Wire the MOSFET control safely

Connect ESP32 GPIO25 to one end of the 100 Ω gate resistor, and connect its other end to the MOSFET GATE. Connect the 10 kΩ gate pulldown between that same GATE point and the ground rail. The resistor makes the MOSFET stay off while the ESP32 starts up.

  • Keep the gate wires short and separate from the thick resistor wires.
  • Do not omit the 10 kΩ resistor: without it, the load can turn on unexpectedly during reset.

4. Add voltage and temperature measurement

Connect INA219 VIN- to the 20 kΩ resistor, connect the other end of that resistor to ADS1115 AIN0, then connect the 10 kΩ voltage-divider resistor from AIN0 to ground. This reduces the cell voltage to a safe value for the ADS1115. Tape the two-lead 10 kΩ NTC thermistor firmly against the insulated side of the cell. Connect one NTC lead to ground and the other NTC lead to ADS1115 AIN1; connect the remaining fixed 10 kΩ resistor from 3V3 to AIN1.

  • Use thermal tape to hold the NTC against the cell body, not across either battery terminal.
  • The 20 kΩ and 10 kΩ voltage-divider resistors must be in the stated order: 20 kΩ from cell sense point to AIN0, 10 kΩ from AIN0 to ground.
  • A divider wired backwards can expose the ADC input to too much voltage and damage it.

5. Connect the shared screen and sensor wires

Connect 3V3 and ground to the ADS1115, INA219, and OLED. Join every SDA pin from these three modules to ESP32 GPIO21, and join every SCL pin to ESP32 GPIO22. Tie ADS1115 ADDR to ground so it uses address 0x48.

  • All three boards share the same two data wires; that is normal.
  • Use 3.3 V for every I²C module so their data wires never rise above the ESP32’s safe 3.3 V level.
  • Make sure VCC and GND are not swapped — swapped power can damage the screen or sensor modules.

6. Connect the SD card, buttons, buzzer, and result lights

Power the MicroSD module from 3V3 and ground. Connect SD MISO to GPIO19, MOSI to GPIO23, SCK to GPIO18, and CS to GPIO4. Connect each push button between its signal pin and ground: START to GPIO32 and STOP to GPIO33. Connect buzzer SIGNAL to GPIO27 and its other lead to ground. For each LED, wire GPIO13 through its 220 Ω resistor to the green LED long leg, GPIO14 through its 220 Ω resistor to the yellow LED long leg, and GPIO26 through its 220 Ω resistor to the red LED long leg; connect all three short legs to ground.

  • The LED long leg is the positive leg; each LED needs its own 220 Ω resistor.
  • Use a 3.3 V-compatible MicroSD module. Some inexpensive 5 V-labelled modules do not work reliably from 3.3 V.
  • Never connect an LED directly to a GPIO; its 220 Ω resistor prevents excessive current.
  • GPIO4 is the MicroSD card-select wire; do not let it touch ground during reset.

7. Inspect before inserting the battery

With USB connected but no battery installed, check that the OLED says READY and all three result LEDs are off. Keep the emergency stop released so its normally-closed contact is connected. Insert only one undamaged protected 18650 cell in the correct holder polarity, then press START for the 30-second low-current test.

  • Use the STOP button for a normal abort; press the red emergency stop immediately if a wire, resistor, MOSFET, or cell becomes unusually hot.
  • The result is a screening result, not a certification of battery safety; visibly damaged, swollen, leaking, or very hot cells must not be tested.
  • Never test loose cells, cells with torn insulation, cells above 4.25 V, or cells below 3.00 V.
  • Do not leave a battery test unattended.

Review all connections

1. Connections between "battery_fixture_1" and "ESP32"

Functionbattery_fixture_1ESP32
powerBAT+ → Latching emergency-stop push button, normally closed contact COMEXT
groundBAT-GND

2. Connections between "estop_1" and "ESP32"

Functionestop_1ESP32
powerNC → 1.5 A inline DC test-path fuse and holder INEXT

3. Connections between "test_fuse_1" and "ESP32"

Functiontest_fuse_1ESP32
powerOUT → Adafruit INA219 High-Side DC Current Sensor VIN+EXT

4. Connections between "ina219_1" and "ESP32"

Functionina219_1ESP32
dataVIN- → Resistor P1EXT
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

5. Connections between "load_resistor_1" and "ESP32"

Functionload_resistor_1ESP32
digitalP2 → IRLZ44N Logic-Level N-Channel MOSFET DRAINEXT

6. Connections between "mosfet_1" and "ESP32"

Functionmosfet_1ESP32
groundSOURCEGND

7. Connections between "voltage_divider_top_1" and "ESP32"

Functionvoltage_divider_top_1ESP32
digitalP1 → Adafruit INA219 High-Side DC Current Sensor VIN-EXT
digitalP2 → ADS1115 16-Bit ADC Module AIN0EXT

8. Connections between "voltage_divider_bottom_1" and "ESP32"

Functionvoltage_divider_bottom_1ESP32
digitalP1 → ADS1115 16-Bit ADC Module AIN0EXT
groundP2GND

9. Connections between "ads1115_1" and "ESP32"

Functionads1115_1ESP32
powerVDD3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22
groundADDRGND

10. Connections between "ntc_fixed_resistor_1" and "ESP32"

Functionntc_fixed_resistor_1ESP32
powerP13V3
digitalP2 → ADS1115 16-Bit ADC Module AIN1EXT

11. Connections between "oled_1" and "ESP32"

Functionoled_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

12. Connections between "microsd_1" and "ESP32"

Functionmicrosd_1ESP32
powerVCC3V3
groundGNDGND
spiMISOGPIO 19
spiMOSIGPIO 23
spiSCKGPIO 18
spiCSGPIO 4

13. Connections between "gate_resistor_1" and "ESP32"

Functiongate_resistor_1ESP32
digitalP1GPIO 25
digitalP2 → IRLZ44N Logic-Level N-Channel MOSFET GATEEXT

14. Connections between "gate_pulldown_1" and "ESP32"

Functiongate_pulldown_1ESP32
digitalP1 → IRLZ44N Logic-Level N-Channel MOSFET GATEEXT
groundP2GND

15. Connections between "start_button_1" and "ESP32"

Functionstart_button_1ESP32
digitalSIGNALGPIO 32
groundGNDGND

16. Connections between "stop_button_1" and "ESP32"

Functionstop_button_1ESP32
digitalSIGNALGPIO 33
groundGNDGND

17. Connections between "buzzer_1" and "ESP32"

Functionbuzzer_1ESP32
digitalSIGNALGPIO 27
groundGNDGND

18. Connections between "reuse_led_resistor_1" and "ESP32"

Functionreuse_led_resistor_1ESP32
digitalP1GPIO 13
digitalP2 → LED ANODEEXT

19. Connections between "reuse_led_1" and "ESP32"

Functionreuse_led_1ESP32
groundGNDGND

20. Connections between "inspect_led_resistor_1" and "ESP32"

Functioninspect_led_resistor_1ESP32
digitalP1GPIO 14
digitalP2 → LED ANODEEXT

21. Connections between "inspect_led_1" and "ESP32"

Functioninspect_led_1ESP32
groundGNDGND

22. Connections between "recycle_led_resistor_1" and "ESP32"

Functionrecycle_led_resistor_1ESP32
digitalP1GPIO 26
digitalP2 → LED ANODEEXT

23. Connections between "recycle_led_1" and "ESP32"

Functionrecycle_led_1ESP32
groundGNDGND

24. Connections between "ntc_1" and "ESP32"

Functionntc_1ESP32
analogSENSE → ADS1115 16-Bit ADC Module AIN1EXT
groundGNDGND

Deploy the firmware

#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <Adafruit_ADS1X15.h>
#include <Adafruit_INA219.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

constexpr int SD_CS_PIN = 4;
constexpr int LOAD_GATE_PIN = 25;
constexpr int START_BUTTON_PIN = 32;
constexpr int STOP_BUTTON_PIN = 33;
constexpr int BUZZER_PIN = 27;
constexpr int REUSE_LED_PIN = 13;
constexpr int INSPECT_LED_PIN = 14;
constexpr int RECYCLE_LED_PIN = 26;

constexpr float DIVIDER_RATIO = 3.0f;       // 20 kOhm top / 10 kOhm bottom divider
constexpr float MIN_START_VOLTAGE = 3.00f;  // Do not test a deeply discharged cell
constexpr float CUTOFF_VOLTAGE = 2.80f;
constexpr float MAX_CELL_TEMP_C = 55.0f;
constexpr unsigned long TEST_DURATION_MS = 30000UL;
constexpr unsigned long SAMPLE_PERIOD_MS = 500UL;
constexpr float NTC_BETA = 3950.0f;
constexpr float NTC_R0 = 10000.0f;
constexpr float NTC_T0_K = 298.15f;

Adafruit_ADS1115 ads;
Adafruit_INA219 ina219;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
bool sdReady = false;
bool testRunning = false;
unsigned long testStartedMs = 0;
unsigned long lastSampleMs = 0;
float openCircuitVoltage = 0.0f;
float minimumVoltage = 99.0f;
float peakCurrentA = 0.0f;
float startTempC = 0.0f;
float lastVoltage = 0.0f;
float lastCurrentA = 0.0f;
float lastTempC = 0.0f;

float readCellVoltage() {
  int16_t raw = ads.readADC_SingleEnded(0);
  float adcVolts = ads.computeVolts(raw);
  return adcVolts * DIVIDER_RATIO;
}

float readTemperatureC() {
  int16_t raw = ads.readADC_SingleEnded(1);
  float nodeVolts = ads.computeVolts(raw);
  if (nodeVolts <= 0.01f || nodeVolts >= 3.29f) return NAN;
  // 10 kOhm fixed resistor is from 3.3 V to the node; NTC is from node to GND.
  float ntcOhms = NTC_R0 * nodeVolts / (3.3f - nodeVolts);
  float invT = (1.0f / NTC_T0_K) + log(ntcOhms / NTC_R0) / NTC_BETA;
  return (1.0f / invT) - 273.15f;
}

float readCurrentA() {
  return ina219.getCurrent_mA() / 1000.0f;
}

void setResultLeds(bool reuse, bool inspect, bool recycle) {
  digitalWrite(REUSE_LED_PIN, reuse ? HIGH : LOW);
  digitalWrite(INSPECT_LED_PIN, inspect ? HIGH : LOW);
  digitalWrite(RECYCLE_LED_PIN, recycle ? HIGH : LOW);
}

void drawStatus(const char *title, const char *line4 = "") {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("PUNARVA 18650 TEST");
  display.println(title);
  display.print("V: "); display.print(lastVoltage, 2); display.println(" V");
  display.print("I: "); display.print(lastCurrentA, 2); display.println(" A");
  display.print("T: "); display.print(lastTempC, 1); display.println(" C");
  display.println(line4);
  display.display();
}

void appendLog(const char *result, float resistanceOhms) {
  if (!sdReady) return;
  File log = SD.open("/punarva.csv", FILE_APPEND);
  if (!log) return;
  log.print(millis()); log.print(',');
  log.print(openCircuitVoltage, 3); log.print(',');
  log.print(minimumVoltage, 3); log.print(',');
  log.print(peakCurrentA, 3); log.print(',');
  log.print(lastTempC, 2); log.print(',');
  log.print(resistanceOhms * 1000.0f, 1); log.print(',');
  log.println(result);
  log.close();
}

void finishTest(const char *forcedResult = nullptr) {
  digitalWrite(LOAD_GATE_PIN, LOW);
  testRunning = false;
  float voltageDrop = openCircuitVoltage - minimumVoltage;
  float resistanceOhms = (peakCurrentA > 0.05f) ? voltageDrop / peakCurrentA : 99.0f;
  float temperatureRise = lastTempC - startTempC;
  const char *result = forcedResult;
  if (result == nullptr) {
    if (minimumVoltage < CUTOFF_VOLTAGE || lastTempC >= MAX_CELL_TEMP_C || resistanceOhms > 0.25f) result = "RECYCLE";
    else if (resistanceOhms > 0.12f || temperatureRise > 8.0f || minimumVoltage < 3.20f) result = "INSPECT";
    else result = "REUSE";
  }
  setResultLeds(strcmp(result, "REUSE") == 0, strcmp(result, "INSPECT") == 0, strcmp(result, "RECYCLE") == 0);
  tone(BUZZER_PIN, 2200, 180);
  lastVoltage = minimumVoltage;
  drawStatus(result, "Remove cell before next test");
  appendLog(result, resistanceOhms);
}

void startTest() {
  lastVoltage = readCellVoltage();
  lastCurrentA = readCurrentA();
  lastTempC = readTemperatureC();
  setResultLeds(false, false, false);
  if (isnan(lastTempC) || lastVoltage < MIN_START_VOLTAGE || lastVoltage > 4.25f) {
    drawStatus("CHECK CELL", "Use 3.00-4.25 V cell");
    tone(BUZZER_PIN, 500, 350);
    return;
  }
  openCircuitVoltage = lastVoltage;
  minimumVoltage = lastVoltage;
  peakCurrentA = 0.0f;
  startTempC = lastTempC;
  testStartedMs = millis();
  lastSampleMs = 0;
  testRunning = true;
  digitalWrite(LOAD_GATE_PIN, HIGH);
}

void setup() {
  pinMode(LOAD_GATE_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(REUSE_LED_PIN, OUTPUT);
  pinMode(INSPECT_LED_PIN, OUTPUT);
  pinMode(RECYCLE_LED_PIN, OUTPUT);
  pinMode(START_BUTTON_PIN, INPUT_PULLUP);
  pinMode(STOP_BUTTON_PIN, INPUT_PULLUP);
  digitalWrite(LOAD_GATE_PIN, LOW);
  setResultLeds(false, false, false);

  Wire.begin(21, 22);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  ads.begin(0x48);
  ads.setGain(GAIN_ONE);
  ina219.begin();
  SPI.begin(18, 19, 23, SD_CS_PIN);
  sdReady = SD.begin(SD_CS_PIN, SPI);
  lastVoltage = readCellVoltage();
  lastCurrentA = readCurrentA();
  lastTempC = readTemperatureC();
  drawStatus("READY", "Press START to test");
}

void loop() {
  if (!testRunning) {
    if (digitalRead(START_BUTTON_PIN) == LOW) {
      delay(25);
      if (digitalRead(START_BUTTON_PIN) == LOW) {
        startTest();
        while (digitalRead(START_BUTTON_PIN) == LOW) delay(5);
      }
    }
    return;
  }

  if (digitalRead(STOP_BUTTON_PIN) == LOW) {
    finishTest("INSPECT");
    while (digitalRead(STOP_BUTTON_PIN) == LOW) delay(5);
    return;
  }

  unsigned long now = millis();
  if (now - lastSampleMs >= SAMPLE_PERIOD_MS) {
    lastSampleMs = now;
    lastVoltage = readCellVoltage();
    lastCurrentA = readCurrentA();
    lastTempC = readTemperatureC();
    if (lastVoltage < minimumVoltage) minimumVoltage = lastVoltage;
    if (lastCurrentA > peakCurrentA) peakCurrentA = lastCurrentA;
    drawStatus("TESTING", "STOP aborts test");

    if (isnan(lastTempC) || lastTempC >= MAX_CELL_TEMP_C) {
      finishTest("RECYCLE");
      return;
    }
    if (lastVoltage <= CUTOFF_VOLTAGE) {
      finishTest();
      return;
    }
  }

  if (now - testStartedMs >= TEST_DURATION_MS) finishTest();
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik