Our consumerist society throws out millions of rechargeable vape cells, but the more I read about it sounds like lithium batteries should explode all the time.
Recap
Vape Cell EV is the idea of a smart cell charging system that can handle cells with unknown histories, charging, power distribution, any safety issues and ultimately if a cell should be removed from a battery of many cells.
INA219
To characterise a cell you need at least three things: voltage, current, and time. Voltage alone tells you roughly where you are on the discharge curve. Current alone tells you the load. Put them together over time and you get energy in milliwatt-hours — which is the actual capacity number you are trying to measure. The INA219 gives you all three from a single cheap I2C chip.
It works by sitting in series with the circuit. A small shunt resistor — 0.1 Ω on the standard breakout module — is placed between VIN+ and VIN−. The INA219 measures the tiny voltage drop across that shunt (10 mV per amp at 0.1 Ω) and from that calculates current. Simultaneously it measures the bus voltage: the voltage at VIN− relative to GND, which is the voltage the load actually sees. The series resistance is small enough that it barely affects the circuit — 0.1 Ω in series with a 10 Ω load is less than 1% overhead.
The first real test was a known 18650 cell discharged through a 10 Ω / 5 W resistor: the INA219 read ~385 mA, ~3.87 V, ~1420 mW. The resistor got warm, as expected — 1.4 W in a 5 W part is entirely within rating. The session tracking in the dashboard accumulates mWh in real time, which means leaving it running through a full discharge gives you the cell capacity directly, without any manual timing or maths.
Wire.begin() isn’t optional
On this platform, Wire2.begin() isn’t just good practice — without it, every I2C call fails immediately regardless of wiring. The Zephyr arduino port uses lazy initialisation: the hardware peripheral doesn’t start until you explicitly call begin(). Nothing about the error messages hints at this; everything just returns 1.
void setup() {
Bridge.begin();
Wire2.begin(); // i2c3: A4=SDA, A5=SCL
// ...
}
Current without voltage
First real test with an 18650 across a 10 Ω resistor: current read correctly at about 196 mA, but bus voltage was 0.012 V — effectively zero. The fix was obvious once the INA219’s measurement model clicked: bus voltage is VIN− relative to GND, so if the battery’s negative terminal isn’t connected to the INA219’s GND pin, the reference floats. Current measurement is differential (VIN+ minus VIN−) and works regardless — voltage measurement doesn’t.
Once a wire ran from the battery negative node to GND: 3.87 V, 385 mA, 1420 mW.
A silent JSON truncation
With everything reading correctly on the MCU, the web dashboard was stuck on NO LINK. The Python side was receiving data but failing to parse it:
[bridge/config] Expecting ',' delimiter: line 1 column 29 (char 28)
Tracing back through the JSON the sketch was producing, the config handler built the sensor address string into a fixed buffer:
char addrStr[6]; snprintf(addrStr, sizeof(addrStr), "\"0x%02X\"", sensors[i].addr);
"0x40" is six characters plus a null terminator — seven bytes. The snprintf quietly truncated the closing ", leaving the JSON ["0x40,"rShunt":...] — which is a parse error exactly at character 28. Bumping the buffer to char addrStr[8] fixed it.
The display
With readings flowing, there’s a live dashboard served directly from the UNO Q’s MPU:
- Per-sensor cards showing bus voltage (amber), current (green), power
- Dual-axis sparklines with fixed scales — 0–6 V and 0–500 mA — so a steady reading looks like a flat line instead of thrashing wall to wall
- Live CSV logging and a download button
- Session tracking: name a trace, press START, watch mWh accumulate in real time

Next
Actual cell characterisation — charge a known 18650 to 4.2 V, discharge it through a measured load down to 3.0 V, and get a real capacity number. Then do the same with salvaged vape cells and see how they compare.