Battery energy storage systems (BESS) at solar farms and C&I sites can fail through thermal runaway: one overheating cell vents gas and triggers a chain reaction across the pack, ending in fire. This project is a self-contained module that sits inside any battery enclosure, needs no connection to the cells or the BMS, and watches the three physical signs a failing cell gives off before the fire: vented gas, a pressure pulse and a temperature rise. It decides on board, from those signs together, whether a cell is failing, and reports every reading and verdict over a checksummed link to a gateway web dashboard.
Component List
DigiKey MyList: click here
| ID | Name | Manufacturer P/N | DigiKey P/N (verify) | Designator | Qty |
|---|---|---|---|---|---|
| 1 l | MAX32630FTHR Feather board (Arm Cortex-M4F, 96 MHz, 2 MB flash, 512 KB RAM), Analog Devices | MAX32630FTHR# | MAX32630FTHR#-ND | U1 | 1 |
| 2 l | MAX32625PICO DAPLink debug adapter (flashing + USB serial), Analog Devices | MAX32625PICO# | MAX32625PICO#-ND | NA | 1 |
| 3 | BME688 gas / VOC, temperature, humidity, pressure breakout, Adafruit 5046 | 5046 | 1528-5046-ND | U2 | 1 |
| 4 | TMP117 ±0.1 °C precision temperature breakout, Adafruit 4821 | 4821 | 1528-4821-ND | U3 | 1 |
| 5 | Honeywell ABP2 gauge pressure sensor, 0–1 psi, I²C, 3.3 V, DIP | ABP2DANT001PG2A3XX | 480-ABP2DANT001PG2A3XX-ND | U4 | 1 |
| 6 | Raspberry Pi 5 | NA | NA | J1 | 1 |
| 7 | IV Silicone tube for the ABP2 pressure port | NA | NA | NA | 1 |
| 8 | Jumper wires, headers, perfboard | NA | NA | NA | 3 |
| 9 | Battery LiPoFe | NA | NA | BT1 | 1 |
Project Explanation, Step by Step
The problem
Battery energy storage systems (BESS) are being installed at solar farms and commercial & industrial (C&I) sites across India, alongside telecom towers, EV charging stations and UPS rooms. They can fail through thermal runaway: one overheating cell vents gas and triggers a chain reaction across the pack, ending in fire. Heat drives chemical reactions that release more heat, the cell vents, and the heat and flame spread to its neighbours. Once a battery fire starts it is very hard to put out.
Existing protection is tied to the pack. The BMS sees only the cells it is wired to, speaks the vendor’s protocol, and differs between makes and chemistries, so a site with packs from several vendors (common at C&I sites and in retrofits) has no single, independent safety layer.
Most installations watch only the BMS (cell voltage and temperature) and a smoke detector in the room. Both react late. A failing cell gives itself away earlier. As it breaks down it vents electrolyte vapour and gases (hydrogen, carbon monoxide, solvent vapours), and the pressure inside a closed enclosure rises. That off-gas stage can come before temperatures climb or smoke appears. Dedicated off-gas detectors exist, but they are expensive and aimed at large installations.
The idea
A low-cost module that is pack-agnostic and self-contained. It sits inside any battery enclosure, whatever the vendor, chemistry (NMC, LFP) or size. It needs no wiring to the cells and no connection to the BMS: it reads the air, the pressure and the temperature of the enclosure, the physical signs every failing cell gives off. It watches three of them:
1. Gas / VOC (Bosch BME688 metal-oxide sensor). Vented electrolyte, H₂ and CO lower its gas resistance.
2. Enclosure pressure (Honeywell ABP2, 0–1 psi gauge). A venting cell gives a pressure pulse.
3. Temperature and its rate of rise (TI TMP117, ±0.1 °C). A sustained climb, or a climb faster than normal.
The key insight: one channel alone is usually an everyday disturbance. Gas alone is usually people, perfume or a cleaning solvent. Pressure alone is a door or the air-conditioning. A slow temperature rise is the sun. A real battery failure disturbs two or more channels at about the same time. So the module raises its battery-event alarm only when two channels go abnormal within the same minute, or when temperature reaches a critical level on its own. That is what keeps it from crying wolf, while still reacting to the earliest signs.
Thermal runaway types and their sensor signatures
Different triggers show up in a different order and mix across the three channels. Arrows are qualitative: ↓ / ↑ = change, ↓↓ / ↑↑ = strong change, – = no clear change. The patterns are the expected ones from published battery abuse tests. This prototype has not been tested on real failing cells.
| Trigger | What happens in the cell | Gas / VOC (BME688) | Enclosure ΔP (ABP2) | Air temp (TMP117) | Order and warning time | What the module sees |
|---|---|---|---|---|---|---|
| Thermal abuse / cooling failure | Whole pack warms; the protective layer on the electrodes breaks down; electrolyte evaporates, then the cell vents | ↓ then ↓↓ at vent | ↑ at vent | ↑ slow, steady from the start | T → gas → ΔP. Minutes to hours | Temperature-rate warning first; event when gas joins in |
| Overcharge | Electrolyte breaks down into gas, the cell swells, heats from within, then vents | ↓↓ at vent | ↑↑ pulse | ↑ moderate to fast | Gas and ΔP before a big air-temperature rise. Minutes | Gas + ΔP + rising T: strongest combined signature |
| Internal short (defect, dendrite, separator failure) t | Local hot spot inside one cell; little outside sign until it vents | ↓↓ sudden | ↑↑ sudden spike | ↑↑ steep, after the gas | Gas ≈ ΔP → T. Seconds to minutes | Off-gas is the earliest outside sign; gas + pressure = event |
| External short | Very high current heats the whole cell | ↓ after heating | ↑ at vent | ↑↑ fast | T → gas → ΔP. Seconds to minutes | Mainly the BMS’s job; the module is an independent backup |
| Mechanical abuse (crush, puncture, drop) | Immediate internal short | ↓↓ at once | ↑↑ at once | ↑↑ quickly | Near-simultaneous. Seconds | All three channels at once: highest confidence |
| Over-discharge, then recharge | Copper dissolves and re-plates, a hidden fault that later shorts | – until failure | – until failure | – until failure | Silent for days or weeks, then like an internal short | Caught at the venting stage |
| Electrolyte leak / cracked seal | Slow solvent vapour, no heating yet | ↓ slow, persistent | – | – | Hours to days ahead | Gas-drift maintenance warning (gas below 80 % for an hour) |
Telling a battery event from everyday disturbances
| Disturbance (not a battery fault) | Gas | ΔP | Temp | RH | How the module treats it |
|---|---|---|---|---|---|
| People, perfume, cleaning solvents, sanitiser | ↓ (can be large) | – | – | ↑ slight | Gas warning only, labelled “gas alone: often people or solvents”. No event |
| Door opening, HVAC switching | – / slight | step, either way | step | step | Pressure warning only, labelled “pressure alone: often a door or HVAC”. No event |
| Sun or daytime heating | – | – | ↑ slow, follows time of day | ↓ | Temperature warning only if it crosses the limits. No event |
| Condensation / water ingress | slight drift | – | – | ↑↑ | High-humidity warning on the dashboard |
Confidence ladder: one channel = warning (with a hint about the likely everyday cause), two channels within a minute = critical “Possible battery event”, temperature ≥ 60 °C = critical on its own.
What makes it different
- Pack-agnostic. No cell taps, no BMS protocol, no vendor integration. The same module goes into any make or chemistry of pack, new or already installed, because it measures the physical effects of a failing cell rather than the pack’s electronics.
- Self-contained. Sensing, detection and the alarm decision all happen on the one module. It does not depend on the BMS, the gateway or the internet to decide that a cell is failing; the gateway is for display and logging.
- Detection on the module, not in the cloud. Every threshold and the two-channel coincidence logic run on the MAX32630 itself (
rules.h), every 3 seconds. The result travels in every data frame, so the decision survives a network outage. - Fusion that cuts false alarms. Single-channel disturbances are reported, explained and kept below the critical level. In 10 hours of real logs (11,788 frames, including deliberate gas and pressure tests), the module raised zero false battery-event alarms, while every individual gas and pressure disturbance was still flagged as a warning.
- One source of truth for thresholds. The limits live in one firmware header. The gateway reads them from that file at deploy time, so the module and the dashboard can never disagree. A replay tool runs the exact firmware code over logged data on a PC, so a threshold change can be checked against real history before flashing.
- Fails safe. A hardware watchdog resets the module if the firmware hangs or a sensor stops answering. Every data frame doubles as a heartbeat, so if frames stop, the gateway raises a critical “No data” alert within 6 s. A dead module can never look healthy.
- Robust link. A compact framed protocol with a CRC-16, a sequence number (to detect lost frames and reboots) and a version field, over three wires (TX, RX, GND). The same frame can be carried over RS-232 / RS-485 to a BMS.
- IoT dashboard. The gateway logs every frame and serves a live web dashboard (HTTPS) with status tiles, history charts and an alert feed, viewable on any phone or PC on the network.
System architecture

How it works, in one cycle (every 3 s)
1. The MAX32630 reads the TMP117, the ABP2 and the BME688. The BME688 runs through Bosch’s BSEC2 library, which also estimates an IAQ index, a CO₂-equivalent and a breath-VOC equivalent from the gas reading.
2. rules.h judges each channel:
- Gas: resistance at the steady 320 °C heater step, as a ratio of its clean-air level (63.1 kΩ, the median of a quiet overnight log). ≤ 60 % = warning, ≤ 35 % = critical. Below 80 % for an hour = gas-drift (leak) warning.
- Pressure: |ΔP| ≥ 50 Pa = warning, ≥ 200 Pa = critical.
- Temperature: ≥ 45 °C = warning, ≥ 60 °C = critical; a rise of ≥ 1 °C/min (least-squares slope over 2 minutes) = rising-fast warning.
3. Coincidence: each channel stays “active” for one minute after it last tripped. Two active channels at once, or temperature critical, sets the battery-event bit and the frame’s alarm flag.
4. The firmware sends one $BESS frame with all readings, the result bits and flags, protected by a CRC-16, to the gateway (and over USB for debugging).
5. The gateway checks the CRC and sequence, logs the frame to CSV, shows the module’s verdicts on the dashboard (with a hint when only one channel is abnormal), and adds link-level alerts: no data, corrupted frames, module rebooted.
Coding Explanation and Configuration
Repository: click here. Full setup steps and board gotchas are in SETUP.md.
1. Module firmware (MAX32630FTHR, Mbed OS, C++)
- Toolchain: Mbed OS with CMake and the GNU Arm Embedded compiler, flashed over the MAX32625PICO with
pyocd.tools/setup_mac.shinstalls everything on macOS;bessflashbuilds and flashes,bessmonopens the serial console. main.cpp: boot, power, sensors, watchdog and the main loop.- The MAX32630FTHR’s header 3V3 pin is off at power-up: it comes from the on-board MAX14690 PMIC’s LDO3, which Mbed OS doesn’t enable.
power_check()turns it on over the PMIC’s I²C bus at every boot. - The I²C pins (P3_4, P3_5) and UART2 pins (P3_0, P3_1) are moved from the 1.8 V to the 3.3 V I/O rail, so the bare ABP2 and the gateway’s 3.3 V UART work directly.
- Watchdog (WDT0): warning interrupt at ~2.8 s (red LED), hardware reset at ~5.6 s. If a sensor fails 3 reads in a row, the firmware stops feeding the watchdog on purpose, so the module resets and re-probes.
- ABP2 zero-offset correction (
ABP2_ZERO_COUNTS), so frames carry true gauge pressure. bme_bsec.cpp: BME688 through Bosch’s BME68x driver and BSEC2. Calibration state is saved in the last flash page and restored at boot.rules.h: the detection logic described above. It has no Mbed dependencies, sotools/check_rules.pycompiles the same file on a PC and replays logged frames through it.bess_proto.h: the data frame encoder.


2. The data protocol ($BESS frame, v4)
One ASCII line per cycle, 115200 baud 8N1:
None
$BESS,4,<seq>,<up_ms>,<flags>,<t117>,<dP>,<T_abp>,<T_bme>,<P>,<RH>,<gas>,<heat>,<i
aq>,<iaq_acc>,<siaq>,<co2eq>,<bvoc>,<bsec_st>,<rules>*<CRC16>
- Integer fixed-point values (0.01 °C, 0.01 Pa, 0.01 %RH), so the MCU never prints floating point.
- An empty field means that sensor failed this cycle; the flags say which.
rules= the module’s verdict bits (gas low/critical, pressure warn/critical, temp high/critical/rising, gas drift, battery event).seqcounts from 0 at boot: a gap = lost frames, back to 0 = module rebooted.- CRC-16/CCITT over every byte between
$and*. - Each frame is also the heartbeat: no valid frame for 6 s (longer than the 5.6 s watchdog reset) = fault.
- Human-readable on a serial terminal, checkable by machine at the gateway, and easy to carry over RS-485 later.
3. Gateway and dashboard (pi/, Python)


bess_dashboard.pyreads the UART (/dev/ttyAMA0), checks CRC and sequence, logs every frame to a daily CSV and every alert toevents.jsonl, and serves the web UI behind nginx (HTTP/HTTPS).- Live updates reach the browser via Server-Sent Events. API:
/api/status,/api/history,/api/events. - Gas, pressure and temperature verdicts come from the module. The dashboard adds BSEC air-quality, humidity and link-health alerts (
config.json). - Runs as a
systemdservice, installed withpi/deploy.sh, which also reads the thresholds out ofrules.h.
4. Tools
tools/bess_rx.py: reference receiver and logger (one CSV per module boot).tools/check_rules.py: replays logs throughrules.hand lists every warning and event period.

Connection Explanation
All sensors share one I²C bus (I2C1, 100 kHz) and are powered from the MAX32630FTHR’s 3V3 pin. Never power the breakouts from VBUS (5 V): their I²C pull-ups go to VIN, which would put the whole bus at 5 V.
| From | To (MAX32630FTHR) | Notes |
|---|---|---|
| BME688 VIN, TMP117 VIN, ABP2 pin 2 (VDD) | 3V3 | enabled by firmware at boot |
| All GNDs, ABP2 pin 1 | GND | |
| BME688 SDI, TMP117 SDA, ABP2 pin 5 (SDA) | P3_4 (SDA) | BME688 board label SDI = SDA |
| BME688 SCK, TMP117 SCL, ABP2 pin 6 (SCL) | P3_5 (SCL) | BME688 board label SCK = SCL |
| ABP2 pins 3 (EOC), 4 | not connected |
I²C addresses: BME688 0x77, TMP117 0x48, ABP2 0x28.
Gateway link (3 wires, 3.3 V logic on both sides, 115200 8N1):
| MAX32630FTHR | Gateway GPIO header |
|---|---|
| P3_1 (UART2 TX) | pin 10 (RXD) |
| P3_0 (UART2 RX) | pin 8 (TXD) |
| GND | pin 6 (GND) |
Pressure port: run the silicone tube from the ABP2 port into the closed enclosure volume, so a venting cell’s pressure pulse reaches it.
Mounting: in a real enclosure, mount the module high (gas and heat rise), away from the direct airflow of cooling fans.
Testing
1. Power-up. bessflash, then bessmon. The boot log shows the PMIC enabling 3V3, an I²C scan finding 0x28, 0x48 and 0x77, and each sensor’s ID. Green LED blink = healthy cycle.
2. Fail-safe. Pull a sensor wire: after 3 failed reads the watchdog is starved, the red LED comes on at ~2.8 s and the board resets at ~5.6 s; the dashboard shows a critical sensor or “No data” alert. Reconnect and it recovers by itself.
3. Link. The dashboard’s frame counter keeps rising, bad frames stay at 0. Unplug the TX wire: “No data from the module” goes critical within 6 s.
4. Single-channel (should warn, not alarm):
- Hold an open bottle of hand sanitiser near the BME688 (no flames nearby): gas warning, labelled “gas alone”.
- Squeeze the ABP2 tube gently: pressure warning, labelled “pressure alone”.
5. Two-channel battery event (should go critical): do both within one minute (sanitiser near the BME688 and squeeze the tube). The dashboard raises “Possible battery event: gas + pressure abnormal within a minute”.
6. Replay. python3 tools/check_rules.py 'logs/*.csv' replays the logs through the firmware code. On 10 hours of real data (11,788 frames, 01:11–11:18 on 15 Oct 2026): 0 battery-event periods; gas warnings at 10:13 and 10:28 (down to 9 % of the clean-air level, sanitiser tests) and pressure warnings at 09:55–09:58 (up to 4,196 Pa, tube tests), each correctly kept as a single-channel warning.
Note: these are safe stand-ins for the signals a venting cell produces. The module has not yet been tested on a real thermal-runaway event; that is the next stage, with a battery test lab.
Real-World Application and Path to Product
- Who needs it: first, BESS at solar farms and C&I sites (factories, warehouses, commercial buildings with solar + storage). Then telecom tower battery banks, EV charging and battery-swap stations, data-centre and hospital UPS rooms. Any battery enclosure that nobody is watching.
- Why pack-agnostic matters commercially: one product for every vendor’s pack. A site owner, EPC contractor or insurer can add an independent safety layer across a mixed fleet without asking each battery vendor for integration, and without voiding warranties by touching the cells.
- How it is deployed: one module per rack or enclosure, retrofitted with no change to the battery or its BMS. Many modules report to one gateway.
- Integration: the battery-event verdict is already in every frame; the next hardware revision adds an isolated dry-contact output to trip the BMS or a breaker, and RS-485 / Modbus for site SCADA. The gateway can forward alerts by SMS or to a cloud dashboard.
- Cost: built from catalogue parts available on DigiKey, at a small fraction of the cost of dedicated off-gas detection systems. (Prototype BOM from MyList: ₹____ per module.)
- Commercial path: LAIKA IIOT TECHNOLOGIES already builds and deploys industrial edge gateways at Indian manufacturing sites. Next: a custom carrier PCB (KiCad design started in
laika-bess-hw), an IP-rated enclosure, a pilot on a live BESS site, validation with a battery test lab, then certification for target markets. - Per-site calibration: the gas clean-air level is set from each enclosure’s own quiet-period log, and the replay tool shows what any threshold change would have done on that site’s history before it is deployed.
Schematics




