Community project
Portable Battery Monitor

This portable battery monitor combines an ESP32 microcontroller with an INA219 current sensor to track voltage and current draw from a rechargeable LiPo battery in real time. The 0.96" OLED display shows live power metrics while a charming animated character responds to button presses, making battery monitoring both functional and fun. The TP4056 charger module handles safe Li-ion charging via USB Type-C, while a boost converter provides stable 5V power to the ESP32 and display.
This guide provides a complete parts list, wiring diagram, and step-by-step assembly instructions to build your own battery monitor. The included firmware demonstrates I2C communication with the sensor and display, button debouncing, and animated graphics. Once assembled and programmed, the device fits into a compact enclosure and serves as a handy tool for testing power consumption on portable projects.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| Adafruit INA219 High-Side DC Current SensorINA219 | 1 | 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. |
| SSD1315 0.96" OLED Display128x64 SPI | 1 | Small 128x64 OLED display module with SSD1315 controller. Supports both 4-wire SPI and I2C communication interfaces. Operates at 3.3V or 5V. Resolution: 128x64 pixels. Available colors: Blue, White, or Blue and Yellow. |
| TP4056 Type-C Li-ion charger with protectionUSB-C | 1 | Single-cell 3.7 V Li-ion/LiPo USB-C charger module. Assumes the protected version with B+/B- battery terminals and OUT+/OUT- protected load terminals. |
| LiPo 3.7V 1000mAh Battery3.7 V (capacity unknown) | 1 | Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging. |
| Boost ConverterSet to 5.0 V | 1 | Small adjustable MT3608-style DC-DC boost converter module for stepping a lower DC input up to a higher rail such as 5V, 9V, or 12V. It is a power-path module with VIN/VOUT terminals, not a GPIO peripheral. |
| Push ButtonPage | 1 | Momentary push button switch |
| Push ButtonStatus | 1 | Momentary push button switch |
| Green status LEDGreen | 1 | Standard low-current indicator LED. GPIO drives it through a 330 ohm series resistor. |
| 330 ohm LED series resistor330 Ω | 1 | Current-limiting resistor in series between ESP32 GPIO and green LED anode. |
| Red low-voltage LEDRed | 1 | Standard low-current indicator LED. GPIO drives it through a 330 ohm series resistor when output voltage is below threshold. |
| 330 ohm LED series resistor330 Ω | 1 | Current-limiting resistor in series between ESP32 GPIO and red LED anode. |
Assembly
7 stepsPrepare the rechargeable power section
With the USB-C charger unplugged, connect the battery positive lead to TP4056 B+ and battery negative lead to B-. Connect TP4056 OUT+ to the MT3608 VIN+ and TP4056 OUT- to MT3608 VIN-.
- Tip: Use the TP4056 OUT terminals, not B+ and B-, to power the boost converter; OUT is the protected load connection.
- Tip: Have an adult check the battery connector polarity before plugging in USB-C.
- ⚠ Never short a Li-ion battery or leave its bare leads touching metal.
- ⚠ Do not let a child connect or charge the battery without adult supervision.
Set the boost converter to 5 V
Before connecting the ESP32, power the TP4056 from USB-C and use a multimeter to adjust the MT3608 output to 5.0 V between VOUT+ and VOUT-. Disconnect USB-C after adjustment.
- Tip: Turn the small adjustment screw very slowly.
- Tip: The ESP32 may be damaged by a boost output set too high.
- ⚠ Do not connect MT3608 VOUT+ to any ESP32 GPIO pin.
- ⚠ Only connect the 5 V output to the ESP32 VIN/5V power input through the INA219 as described next.
Add the power meter and ESP32 supply
Wire MT3608 VOUT+ to INA219 VIN+. Wire INA219 VIN- to the ESP32 VIN/5V pin. Connect MT3608 VOUT- to ESP32 GND. Connect INA219 VCC to ESP32 3V3 and INA219 GND to ESP32 GND. This places the INA219 in series with the 5 V supply so Buddy can measure its own current.
- Tip: All ground connections must be common: boost VOUT-, ESP32 GND, OLED GND, INA219 GND, buttons, and LED cathodes.
- Tip: The INA219 signal supply is 3.3 V, even though it measures the 5 V power line.
- ⚠ Keep VIN+ and VIN- in the stated order; reversing them makes current readings negative.
- ⚠ Do not power the ESP32 VIN/5V pin from both the boost module and a USB cable at the same time.
Connect the OLED face display
Connect OLED VCC to ESP32 3V3 and OLED GND to ESP32 GND. Connect OLED CLK to GPIO 18, DIN to GPIO 23, CS to GPIO 27, DC to GPIO 17, and RES to GPIO 16.
- Tip: A 7-pin SPI OLED normally labels its data pin DIN or MOSI; connect the pin labeled DIN/MOSI to GPIO 23.
- Tip: Keep the short display wires tidy so the face does not flicker.
- ⚠ Do not connect a 3.3 V-only OLED VCC pin to the 5 V boost rail.
- ⚠ If the module labels differ, use the printed functions: CLK/SCK, DIN/MOSI, CS, DC, and RES/RST.
Connect the INA219 I2C signals
Connect INA219 SDA to ESP32 GPIO 21 and INA219 SCL to ESP32 GPIO 22. Its VCC and GND connections were made in the power step.
- Tip: SDA and SCL are different from the OLED DIN and CLK pins because the OLED is used in SPI mode.
- Tip: Most INA219 breakout boards include the needed I2C pull-up resistors.
- ⚠ Never connect the INA219 VIN+ or VIN- terminals to GPIO 21 or GPIO 22.
Add Buddy's buttons and status lights
For the Page button, connect one terminal to GPIO 32 and the other to GND. For the Status button, connect one terminal to GPIO 33 and the other to GND. Connect GPIO 25 to one end of the first 330 ohm resistor, its other end to the green LED anode (long lead), and the green LED cathode (short lead) to GND. Repeat with GPIO 26, the second 330 ohm resistor, and the red LED.
- Tip: The firmware uses the ESP32's internal pull-up resistors, so each button needs only a GPIO-to-GND connection.
- Tip: Green means the measured 5 V rail is healthy; red means it is low.
- ⚠ Every LED needs its own 330 ohm series resistor. Do not connect an LED directly to a GPIO.
- ⚠ If an LED does not light, turn off power and reverse that LED.
Power up and make a Buddy enclosure
Ask an adult to inspect every power wire. Turn on power, then use Schematik's Deploy button to flash the project. Press Page to cycle Happy, Silly, Sleepy, and Wow faces. Press Status to show or hide the power meter. Mount the OLED behind a cardboard or plastic face window, leaving the charger USB-C port accessible.
- Tip: Label the two buttons with a drawn smiley face and a lightning bolt.
- Tip: A cardboard box makes a simple safe case, but keep metal foil and loose wires away from the battery.
- ⚠ Stop using the project if the battery, charger, or boost converter becomes hot, swells, smells unusual, or is physically damaged.
- ⚠ Charge only while an adult is present and place the project on a non-flammable surface.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| EXT | battery_1 +V → TP4056 Type-C Li-ion charger with protection B+ | power |
| EXT | battery_1 GND → TP4056 Type-C Li-ion charger with protection B- | ground |
| EXT | tp4056_1 IN+ → USB-C 5 V charging cable | power |
| EXT | tp4056_1 IN- → USB-C charging cable ground | ground |
| EXT | tp4056_1 OUT+ → Boost Converter VIN+ | power |
| EXT | tp4056_1 OUT- → Boost Converter VIN- | ground |
| EXT | boost_1 VOUT+ → Adafruit INA219 High-Side DC Current Sensor VIN+ | power |
| GND | boost_1 VOUT- | ground |
| VIN | ina219_1 VIN- | power |
| 3V3 | ina219_1 VCC | power |
| GND | ina219_1 GND | ground |
| GPIO 21 | ina219_1 SDA | i2c |
| GPIO 22 | ina219_1 SCL | i2c |
| 3V3 | oled_1 VCC | power |
| GND | oled_1 GND | ground |
| GPIO 18 | oled_1 CLK | i2c |
| GPIO 23 | oled_1 DIN | i2c |
| GPIO 27 | oled_1 CS | data |
| GPIO 17 | oled_1 DC | data |
| GPIO 16 | oled_1 RES | data |
| GPIO 32 | button_page_1 SIGNAL | digital |
| GND | button_page_1 GND | ground |
| GPIO 33 | button_output_1 SIGNAL | digital |
| GND | button_output_1 GND | ground |
| GPIO 25 | resistor_ok_1 1 | digital |
| EXT | resistor_ok_1 2 → Green status LED A | digital |
| GND | led_ok_1 K | ground |
| GPIO 26 | resistor_low_1 1 | digital |
| EXT | resistor_low_1 2 → Red low-voltage LED A | digital |
| GND | led_low_1 K | ground |
Firmware
ESP32#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_INA219.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define INA_SDA 21
#define INA_SCL 22
#define OLED_CLK 18
#define OLED_DIN 23
#define OLED_CS 27
#define OLED_DC 17
#define OLED_RES 16
#define PAGE_BUTTON 32
#define STATUS_BUTTON 33
#define GREEN_LED 25
#define RED_LED 26
// Hoisted type definitions
enum BuddyMood { HAPPY, SILLY, SLEEPY, SURPRISED };
// Forward declarations
bool pressed(uint8_t pin, unsigned long &lastPress);
void drawEye(int x, int y, bool closed, bool surprised);
void drawMouth(BuddyMood mood, int y);
void drawBuddy();
void drawStatus();
void updateMeter();
Adafruit_INA219 ina219;
Adafruit_SSD1306 display(128, 64, OLED_DIN, OLED_CLK, OLED_DC, OLED_RES, OLED_CS);
BuddyMood selectedMood = HAPPY;
float busVoltage = 0.0f;
float loadCurrent = 0.0f;
unsigned long lastMeterRead = 0;
unsigned long lastAnimation = 0;
unsigned long pagePressedAt = 0;
unsigned long statusPressedAt = 0;
bool blinkEyes = false;
bool showStatus = false;
bool sensorFound = false;
bool pressed(uint8_t pin, unsigned long &lastPress) {
if (digitalRead(pin) == LOW && millis() - lastPress > 250) {
lastPress = millis();
return true;
}
return false;
}
void drawEye(int x, int y, bool closed, bool surprised) {
if (closed) {
display.drawLine(x - 10, y, x + 10, y, SSD1306_WHITE);
return;
}
int height = surprised ? 20 : 16;
display.fillRoundRect(x - 10, y - height / 2, 20, height, 7, SSD1306_WHITE);
display.fillCircle(x, y, surprised ? 3 : 4, SSD1306_BLACK);
if (!surprised) display.fillCircle(x + 1, y - 1, 1, SSD1306_WHITE);
}
void drawMouth(BuddyMood mood, int y) {
if (mood == HAPPY) {
display.drawCircle(64, y - 8, 13, SSD1306_WHITE);
display.fillRect(49, y - 21, 30, 13, SSD1306_BLACK);
} else if (mood == SILLY) {
display.drawLine(51, y - 4, 60, y + 3, SSD1306_WHITE);
display.drawLine(60, y + 3, 69, y - 3, SSD1306_WHITE);
display.drawLine(69, y - 3, 78, y + 4, SSD1306_WHITE);
display.fillRoundRect(59, y + 3, 10, 6, 2, SSD1306_WHITE);
} else if (mood == SLEEPY) {
display.drawLine(52, y, 76, y, SSD1306_WHITE);
} else {
display.drawCircle(64, y - 1, 7, SSD1306_WHITE);
}
}
const char *moodName(BuddyMood mood) {
switch (mood) {
case HAPPY: return "HAPPY";
case SILLY: return "SILLY";
case SLEEPY: return "SLEEPY";
default: return "WOW!";
}
}
void drawBuddy() {
bool lowPower = sensorFound && busVoltage < 4.75f;
BuddyMood faceMood = lowPower ? SLEEPY : selectedMood;
bool closed = blinkEyes || faceMood == SLEEPY;
display.clearDisplay();
display.drawRoundRect(1, 1, 126, 62, 7, SSD1306_WHITE);
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(8, 7);
display.print("BUDDY: ");
display.print(lowPower ? "NEEDS CHARGE" : moodName(faceMood));
drawEye(42, 33, closed, faceMood == SURPRISED);
drawEye(86, 33, closed, faceMood == SURPRISED);
drawMouth(faceMood, 52);
display.setCursor(10, 55);
display.print("PAGE=mood STATUS=meter");
display.display();
}
void drawStatus() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println("BUDDY POWER METER");
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
if (!sensorFound) {
display.setCursor(0, 22);
display.println("INA219 not found.");
display.println("Check SDA/SCL wiring.");
} else {
display.setTextSize(2);
display.setCursor(0, 16);
display.print(busVoltage, 2);
display.println("V");
display.setCursor(0, 38);
display.print(loadCurrent, 0);
display.println("mA");
display.setTextSize(1);
display.setCursor(88, 48);
display.print(busVoltage * loadCurrent / 1000.0f, 2);
display.print("W");
}
display.setTextSize(1);
display.setCursor(0, 55);
display.print("Press STATUS for Buddy");
display.display();
}
void updateMeter() {
if (!sensorFound || millis() - lastMeterRead < 500) return;
lastMeterRead = millis();
busVoltage = ina219.getBusVoltage_V();
loadCurrent = ina219.getCurrent_mA();
if (loadCurrent < 0) loadCurrent = 0;
bool lowPower = busVoltage < 4.75f;
digitalWrite(RED_LED, lowPower ? HIGH : LOW);
digitalWrite(GREEN_LED, lowPower ? LOW : HIGH);
}
void setup() {
pinMode(PAGE_BUTTON, INPUT_PULLUP);
pinMode(STATUS_BUTTON, INPUT_PULLUP);
pinMode(GREEN_LED, OUTPUT);
pinMode(RED_LED, OUTPUT);
digitalWrite(GREEN_LED, LOW);
digitalWrite(RED_LED, LOW);
Wire.begin(INA_SDA, INA_SCL);
sensorFound = ina219.begin();
if (!display.begin(SSD1306_SWITCHCAPVCC)) {
while (true) { delay(100); }
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(18, 20);
display.println("Hi! ^_^ ");
display.display();
delay(1200);
}
void loop() {
updateMeter();
if (pressed(PAGE_BUTTON, pagePressedAt)) {
selectedMood = static_cast<BuddyMood>((selectedMood + 1) % 4);
showStatus = false;
}
if (pressed(STATUS_BUTTON, statusPressedAt)) {
showStatus = !showStatus;
}
if (millis() - lastAnimation > 180) {
lastAnimation = millis();
blinkEyes = !blinkEyes;
if (showStatus) drawStatus();
else drawBuddy();
}
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
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.