Remote Cabinet Backup Battery Sentinel

This reference application is intended to provide inspiration and help you get started quickly. It uses specific hardware choices that may not match your own implementation. Focus on the sections most relevant to your use case. If you'd like to discuss your project and whether it's a good fit for Blues, feel free to reach out.
Building with an AI assistant? Point it at this accelerator's firmware — firmware/cabinet_battery_sentinel/ — so it can learn how the device works and help you adapt it to your hardware.
This project is a pack-level sentinel — a Blues battery management systems reference design — for the backup battery inside a traffic-signal controller, roadside IoT gateway, industrial RTU, or equipment cabinet running on a 12 V or 24 V positive-referenced DC bus. A Blues Notecard Cell+WiFi and Notecarrier CX, paired with a precision current/voltage monitor and a surface-mounted thermistor, continuously measure pack voltage, bidirectional current, surface temperature, and state-of-charge — with proactive alerts on charger faults, elevated float current (a reliable early indicator of VRLA capacity degradation weeks before failure. See §8 for chemistry-specific caveats), mains power loss detected within one sample interval, and a low-SoC threshold trip. Over cellular, independent of every piece of equipment the battery is supposed to protect.
1. Project Overview
The problem. A municipal traffic engineer responsible for hundreds of signalized intersections faces a quiet, expensive problem: the sealed backup battery inside every cabinet — a VRLA (valve-regulated lead-acid) or LFP (lithium iron phosphate) pack designed to keep the signal running through a mains outage — is almost universally untested until the day it fails. A healthy-looking battery can have lost 60% of its usable capacity to sulfation while still holding nominal open-circuit voltage. A single shorted cell in a 12 V six-cell VRLA string depresses the pack voltage, but the charger compensates by pushing more current, masking the fault entirely from any simple voltage-only check. The same pattern plays out in roadside IoT gateways, industrial RTU cabinets, and telecom shelters — whoever owns the site only finds out the battery is dead at the worst possible moment.
The failure signal that does show up early is float current. A healthy VRLA battery at full charge draws only a few milliamps per 100 Ah from the charger — just enough to overcome self-discharge and electrochemical leakage. When internal resistance climbs due to sulfation or plate damage, the charger must push more current continuously just to hold voltage. Elevated float current is a reliable weeks-in-advance indicator that a battery is headed toward failure. It is a pack-level aggregate — it reveals that the string as a whole is degrading, which is often caused by one or more weakened cells, but it does not identify the specific cell or quantify per-cell imbalance. Distinguishing individual failed cells requires per-cell voltage monitoring hardware not included in this design (see §10). Nobody is watching pack float current, because nobody has instrumented it.
This project instruments it. A precision bidirectional current monitor sits in series with the battery's positive terminal, measuring float current with milliamp resolution. When the charger current reverses and the battery starts discharging — because mains power failed — the Notecard fires an immediate alert. When float current climbs steadily over weeks, the hourly summary captures the trend for a downstream analytics system to act on.
Chemistry scope. The float-current capacity-degradation signal is a VRLA mechanism. LFP batteries do not sulfate, and elevated float current on an LFP pack indicates a charger or BMS configuration fault rather than cell-level degradation. LFP installations still benefit fully from four of the six battery-condition alert modes this design provides: power-outage detection (current reversal), pack voltage bounds checking, surface temperature alerting, and low-SoC threshold tripping. Float-current trending as a capacity-health proxy, however, does not apply to LFP without chemistry-specific calibration. See §8 for per-alert chemistry guidance and LFP threshold commissioning Notes.
Why Notecard. The Notecard's independence from site infrastructure is the fundamental feature here. A traffic-cabinet controller, a roadside LoRaWAN gateway, or a roadside remote terminal unit all have their own modems and radios, but those are exactly the devices the backup battery is supposed to keep running during a mains failure. You cannot use the site's LTE modem to report that the site's LTE modem just went down because the backup battery was dead.
New to Blues?
Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.
Learn what we do in this 3-minute video that walks you through how we simplify wireless connectivity.
The Notecard manages its own cellular session against the supported carrier networks worldwide via its embedded global SIM, on a power path independent of the site equipment. When mains fails, the sentinel continues running from the cabinet battery bus through the DC-DC converter — which is precisely what allows it to observe the discharge in real time: voltage sag under actual site load, current draw, duration, and eventual recovery. The Notecarrier CX's onboard LiPo charger adds a reporting tail for the end-of-discharge case: once the cabinet battery is deeply depleted, the bus falls below the DC-DC converter's minimum input voltage, or the battery is disconnected entirely, the 2000 mAh LiPo takes over and extends cellular reporting beyond the battery's own capacity. If the sentinel needs to be fully independent of the monitored battery from the moment of mains failure, not riding on it at all — a separate, isolated power source is required. That discharge curve — only available because the sentinel kept running while everything else went dark — is the most valuable diagnostic data you can collect about backup battery health.
Deployment scenario. A compact enclosure mounted inside a traffic-signal controller housing, roadside IoT gateway enclosure, industrial RTU cabinet, or telecom equipment shelter — installations running on a 12 V or 24 V positive-referenced DC bus. The Adafruit INA228 shunt monitor wires in series with the battery's positive terminal; the NTC thermistor is affixed to the battery case surface with thermal adhesive. A DC-DC step-down module converts the cabinet bus voltage to regulated 5 V for the Notecarrier CX; a 2000 mAh LiPo on the JST connector is charged from that 5 V supply; during a mains outage the sentinel continues running from the cabinet battery via the DC-DC converter, with the LiPo taking over only when the monitored battery is deeply depleted or the bus drops below the converter's input range. The Notecard's cellular antenna routes out of the enclosure via u.FL pigtail to a patch or magnetic-mount antenna on the cabinet exterior.
2. System Architecture
Device-side responsibilities. Every two minutes the STM32L433 host on the
Notecarrier CX wakes for a few seconds, samples the battery, decides whether
anything has gone wrong, and goes back to sleep. In those seconds it reads pack
voltage and bidirectional current from the INA228 over Qwiic, picks up surface
temperature from the NTC thermistor on A0, and evaluates six battery-condition
rules plus one sensor-health check. Any tripped rule becomes an alert
Note marked sync:true for immediate delivery.
Window statistics accumulate in a state struct that simply lives in RAM: between
samples the host sleeps in STM32 STOP2 (a few µA, RAM retained) until the
Notecard's ATTN pin wakes it via
card.attn.
Window-average power (voltAvg × currAvg) is derived at summary time, not
sampled per-read from the INA228 power register.
Notecard responsibilities. The Notecard owns the radio so the host never has
to. It manages its own cellular session against supported carrier networks
worldwide via the embedded global SIM, queues Notes in on-device storage, opens
a session on the configured
hub.set outbound
cadence (default 60 minutes), and short-circuits that cadence whenever a
sync:true alert lands; those go out immediately. On the inbound side it pulls
down environment
variable
updates from the Blues Notehub cloud service, so
the field operator can retune any threshold or either cadence without
reflashing.
Notehub responsibilities. Everything that leaves the cabinet lands in
Notehub, which ingests and stores each event and runs the
project's routes. The two Notefiles — battery_summary.qo for periodic
telemetry and battery_alert.qo for threshold trips — stay deliberately
separate so a route can fan summaries into a long-term analytics store while
pushing alerts straight to whoever is on call.
Routing to the cloud (high level only). Notehub supports HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations; route setup is project-specific. See the Notehub routing docs; this project ships no specific downstream endpoint.
3. Technical Summary
-
Notehub: create a Notehub project and copy the ProductUID.
-
Wire the bench rig: Notecarrier CX + Notecard MBGLW + INA228 on Qwiic + NTC divider on A0 + LiPo on JST +
ATTN→D5jumper. Full pinout in §5. -
Edit one line: set
PRODUCT_UIDinfirmware/cabinet_battery_sentinel/cabinet_battery_sentinel_helpers.h. -
Flash via CLI:
arduino-cli lib install "Blues Wireless Notecard" "STM32duino Low Power" "STM32duino RTC" "Adafruit INA228" arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/cabinet_battery_sentinel/ arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none -p /dev/cu.usbmodem* firmware/cabinet_battery_sentinel/The
usb=noneboard option disables the USB CDC stack, which must be off for the host to sleep (see §7.1). -
Watch for success — open Notehub → Events tab. You know it's working when:
_session.qoappears within 2–3 minutes; this confirms the Notecard reached Notehub over cellularbattery_summary.qoappears within 60 minutes; this is your hourly health summary (sample JSON below)battery_alert.qoappears immediately if any alert condition trips (sample JSON below)
If you don't see
_session.qoafter 5 minutes, check your PRODUCT_UID matches your Notehub project exactly and verify cellular coverage at your location. See §10 Troubleshooting if the Notecard never reaches Notehub.Example
battery_summary.qo(healthy 12 V VRLA at float):{ "volt_v": 13.65, "curr_ma": 12.5, "power_mw": 170.6, "charge_ah": 0.0125, "soc_pct": 97.3, "temp_c": 24.3, "volt_min_v": 13.52, "curr_min_ma": 0.0, "temp_max_c": 25.1, "samples": 30 }Example
battery_alert.qo(power outage event):{ "alert": "power_outage", "volt_v": 12.4, "curr_ma": -3200.0, "temp_c": 25.5 }
4. Hardware Requirements
| Part | Qty | Rationale |
|---|---|---|
| Notecarrier CX | 1 | Compact carrier with an onboard STM32L433 host MCU, Qwiic I2C port, and an onboard JST LiPo charger circuit — essential for keeping the sentinel powered through the outages it's built to detect. The host sleeps in STOP2 between samples and the Notecard's ATTN pin wakes it. Accepts regulated 4.5–5.5 V on the +VBAT header pin. |
| Notecard Cell+WiFi (MBGLW) (datasheet) | 1 | Cellular uplink that manages its own cellular session against supported carrier networks worldwide via embedded global SIM; WiFi acts as an opportunistic fallback for sites that happen to have rooftop coverage. |
| Blues Mojo | 1 | BENCH VALIDATION ONLY — not deployed in production. Splice inline on the 5 V power path during bring-up to measure the complete device's idle baseline, sensor-wake current, and cellular-session bursts. See §8 for analysis and §4 for wiring. Remove before field deployment. |
| Adafruit INA228 Power Monitor Breakout (#5832) | 1 | 20-bit I2C precision monitor measuring bus voltage up to 85 V and bidirectional current through its onboard 15 mΩ shunt. The onboard shunt handles up to approximately 8 A continuous — appropriate for installations where peak discharge current does not exceed that limit (typically batteries up to roughly 40 Ah at a C/5 discharge rate). For higher-current installations, use the breakout's external-shunt footprint with a lower-value busbar shunt. |
| 10 kΩ NTC thermistor, β=3950, waterproof probe | 1 | Surface-mounted to the battery case; detects elevated case temperature that can signal charging faults, poor cabinet ventilation, or abnormal battery conditions — and provides a per-sample temperature for the hourly summary. |
| 10 kΩ 1% resistor | 1 | Pull-up resistor in the thermistor voltage divider. |
| 3.7 V 2000 mAh LiPo battery, JST PH connector (e.g. Adafruit #2011) | 1 | Reporting-tail backup for the end-of-discharge case. During a normal mains outage the sentinel runs from the cabinet battery bus via the DC-DC converter; the LiPo takes over only after the monitored battery is deeply depleted or the bus drops below the converter's minimum input. The Notecarrier CX charges it from the regulated 5 V supply and switches automatically. |
| 5 V regulated DC-DC step-down module — 12 V systems: RECOM R-78E5.0-1.0 (8–28 V input, 1 A) | 1 | Converts the 12 V cabinet bus to the regulated 4.5–5.5 V required by the Notecarrier CX +VBAT pin. The 8–28 V input range comfortably covers a 12 V VRLA or LFP charger bus at float and absorption. |
| 5 V regulated DC-DC step-down module — 24 V systems: Pololu D24V50F5 (6–38 V input, 5 A) | 1 | Use in place of the RECOM module when the cabinet bus is 24 V nominal (float ~27–28 V, absorption up to ~29 V). The 6–38 V input range covers 24 V VRLA and LFP charger buses with ample headroom. |
| Inline fuse holder with 1 A 32 V blade fuse (ATC/ATM automotive style or equivalent) | 1 | Safety-critical. Install in series on the positive-bus cable between the battery bus (+) and the DC-DC converter input. Sized for 1 A — well above the sentinel's ≤500 mA peak draw from a 12 V or 24 V bus during a cellular session, and will clear before wiring fault damage. The INA228 shunt path (battery positive to load bus) carries the full site load current and must be installed on a battery branch that is already protected by an upstream circuit breaker or fuse rated for the expected load current — see §5. |
| u.FL to SMA bulkhead pigtail, ~150 mm (e.g. Adafruit #851) | 1 | Routes the Notecard's cellular antenna connection from the u.FL footprint on the Notecarrier CX to an SMA bulkhead fitting in the enclosure wall. Uses RG178 coax; verify the SMA connector sex matches your antenna before ordering. |
| LTE cellular antenna with SMA connector, panel-mount or magnetic-mount (e.g. SparkFun CEL-16432, 698 MHz–2.7 GHz, 2.3 dBi) | 1 | Mount on the cabinet exterior for reliable LTE Cat-1 bis coverage. A rubber-duck or internal antenna inside a sealed metal enclosure will not maintain consistent signal. |
| Qwiic cable, 100 mm or 200 mm | 1 | Connects the INA228 breakout to the Notecarrier CX Qwiic port. |
| ABS enclosure, IP54 or better (e.g. Hammond 1591XXTSFLBK, ~123 × 83 × 61 mm) | 1 | Houses the Notecarrier CX assembly inside the cabinet. The 1591XXTSFLBK provides ample internal volume for the board stack and wiring, with a clear polycarbonate lid for visual status checks without opening. Add cable glands for the antenna pigtail, thermistor probe, shunt wires, and power leads. |
| Thermal adhesive tape (e.g., 3M 8810) | 1 | Affixes the thermistor probe firmly to the battery case surface for accurate temperature readings. |
All Blues hardware ships with an active SIM including 500 MB of data and 10 years of service: no activation fees, no monthly commitment.
Safety and Installation Requirements (Read Before Wiring)
Safety-critical step: inline fuse. Install a 1 A fuse in series on the positive-bus cable between the battery bus (+) and the DC-DC converter input (the sentinel's own power supply). Battery strings and busbars can deliver thousands of amperes of short-circuit current; the fuse sized for 1 A — well above the sentinel's ≤500 mA peak draw during a cellular session — will clear a wiring fault before causing arc damage or fire.
INA228 shunt installation. The INA228 sits in series on the battery's positive terminal and must be installed on a battery branch that already has an upstream circuit breaker or fuse rated for the expected load current. Do not install the INA228 shunt on a battery segment with no existing overcurrent protection.
Lockout/tagout. Before making any connections to an energised battery bus: isolate and lock out the battery charger and all parallel discharge paths (lockout/tagout procedure), use insulated tools, remove metallic jewelry and watchbands, and confirm the bus is de-energized before touching conductors. For cabinet installations with multiple parallel battery strings, all strings must be isolated simultaneously before working in the positive-bus circuit.
This BOM targets a positive-referenced DC bus — the INA228 high-side sensing
topology and the non-isolated 5 V step-down converter both reference the
negative bus as circuit ground. It covers 12 V and 24 V positive-referenced
VRLA and LFP charger buses (e.g., 12 V VRLA at 13.5–14.7 V float; 24 V VRLA at
27–29.4 V float). For 24 V systems, update volt_min_v and volt_max_v to
match your charger's float window — see §6 — and use the Pololu D24V50F5 listed
in the BOM. Also verify that the expected peak discharge current is within the
shunt's rating (≤8 A for the onboard 15 mΩ shunt; use an external shunt for
higher-current installations).
5. Wiring and Assembly
Safety reminder: Review the safety requirements in §4 before proceeding. Ensure the inline fuse is installed on the sentinel's power supply and the INA228 shunt is on a battery branch with upstream overcurrent protection.
All host I/O lands on the Notecarrier
CX dual 16-pin header
and Qwiic connector, and a short jumper between ATTN and D5 on that header
lets the Notecard wake the sleeping host. The Notecard Cell+WiFi seats into the
M.2 slot.
Cabinet bus to Notecarrier CX power chain:
The cabinet battery bus (typically 12–15 V for 12 V VRLA/LFP, or 25–29 V for 24 V VRLA/LFP charger buses) is not directly compatible with the Notecarrier CX's +VBAT input, which requires regulated 4.5–5.5 V. A DC-DC step-down module bridges the two:
Cabinet battery bus (+) → 1 A inline fuse → DC-DC module input (+)
DC-DC module output (5 V regulated) → Notecarrier CX +VBAT header pin
Cabinet battery bus (–) / cabinet ground → Notecarrier CX GND header pinMount the DC-DC module inside the enclosure and verify its input-voltage range covers your cabinet supply before wiring. The Notecarrier CX's USB-C connector is an equivalent 5 V power entry point if a USB-C cable is more convenient than the header pin for your enclosure layout. See the Notecarrier CX datasheet for +VBAT and GND pin locations on the dual 16-pin header.
The 2000 mAh LiPo on the JST connector is charged from the regulated 5 V supply whenever the cabinet bus is present. During a normal mains outage the sentinel continues drawing power from the cabinet battery via the DC-DC converter — the LiPo does not take over yet. This is intentional: the sentinel is riding on the battery it monitors, directly observing the discharge as it happens. The LiPo becomes the primary power source only after the monitored battery is deeply depleted, the bus voltage drops below the DC-DC converter's minimum input threshold, or the battery is disconnected — extending the reporting tail beyond the battery's own capacity. If the design requirement is full electrical independence from the monitored battery from the moment mains fails, an isolated power supply fed from a separate source is needed instead.
INA228 battery power path (high-side current sensing):
The INA228 measures the current flowing in or out of the battery by sitting in series with the battery's positive terminal; this topology is called high-side sensing.
- Battery (+) terminal → INA228
V+pad (connects to the shunt IN+ input) - INA228
V–pad → cabinet load bus (+) - Battery (–) terminal → cabinet load bus (–) / circuit ground
The 15 mΩ shunt is internal to the Adafruit #5832 breakout; no external shunt
resistor is required for currents up to approximately 8–10 A continuous. For
installations where peak discharge current exceeds 8 A — a 100 Ah battery at a
C/5 discharge rate draws 20 A — bypass the internal shunt via the breakout's
external-shunt footprint and wire a lower-value shunt (e.g. 1–2 mΩ), updating
INA228_SHUNT_OHMS and INA228_MAX_CURRENT_A in firmware to match. See §11
(Limitations) for further details.
INA228 to Notecarrier CX (I2C via Qwiic):
- INA228 Qwiic connector → Notecarrier CX Qwiic connector (3.3 V, GND, SDA, SCL; Qwiic cable)
The Notecarrier CX has onboard I2C pull-ups; no additional pull-up resistors are required.
NTC thermistor voltage divider (A0):
- Notecarrier CX +3V3 → 10 kΩ 1% resistor → Notecarrier CX A0
- Notecarrier CX A0 (same node as above) → NTC thermistor leg 1
- NTC thermistor leg 2 → Notecarrier CX GND
Affix the thermistor probe to the battery case surface using thermal adhesive tape. For multi-cell packs, position it near the geometric center of the pack where heat from an internal fault tends to be highest.
ATTN-to-D5 jumper (host sleep):
Fit a short jumper between the ATTN and D5 pins on the Notecarrier CX
header — both are on the same 16-pin header. The Notecard pulls ATTN low while
the host sleeps and raises it when the next sample is due; the rising edge on
D5 wakes the host from STOP2. Leave the Notecarrier CX EN pin unconnected:
on the CX it enables the shared 3.3 V VIO rail (host, Qwiic, 3V3_OUT, and
Notecard I/O), so driving it from ATTN would brown out the whole board rather
than sleep the host. See Putting a Host to Sleep Between Sensor
Readings
for the full pattern.
LiPo backup battery:
- 2000 mAh LiPo → Notecarrier CX JST PH battery connector
The Notecarrier CX charges the LiPo from the regulated 5 V supply on +VBAT. The transition from external power to LiPo is automatic; no firmware change is needed.
Notecard:
Insert the Notecard Cell+WiFi into the Notecarrier CX M.2 slot and secure with the mounting screw. Connect the u.FL to SMA pigtail to the antenna footprint on the Notecard, route it to an SMA bulkhead fitting in the enclosure wall, and attach the external antenna on the cabinet exterior. A rubber-duck antenna inside a sealed steel cabinet will not maintain reliable LTE Cat-1 bis coverage.
Mojo (bench validation only):
For bench bring-up, splice the Mojo inline between the 5 V DC-DC module output and the Notecarrier CX +VBAT input — do not connect it directly to a 12 V or 24 V cabinet bus. Mojo measures the current consumed by the entire assembled device on the 5 V power rail.
I²C topology: this firmware does not read Mojo's coulomb counter over I²C; Mojo is inline power measurement only. The INA228 connects directly to the Notecarrier CX Qwiic connector as described above — Mojo's Qwiic port does not need to be wired for this bring-up.
Remove Mojo from the power path for production deployment; see §9.
6. Notehub Setup
-
Create a project. Sign up at notehub.io and create a project. Copy the ProductUID; it looks like
com.your-company.your-name:cabinet-battery-sentinel. -
Set the ProductUID in firmware. Open
cabinet_battery_sentinel_helpers.hand replace the empty string on the#define PRODUCT_UID ""line with your value. -
Claim the Notecard. Power the assembled unit. On first cellular connection the Notecard associates with your Notehub project automatically. The device appears in the Devices tab within a few minutes; no manual claim step is required.
-
Create a Fleet. Fleets group devices for shared environment variable configuration and routing. The natural organization here is by battery chemistry and voltage — one fleet for 12 V VRLA roadside cabinets, another for 24 V VRLA or LFP installations — because the safe float voltage windows differ materially between chemistries. Smart Fleets can automate fleet assignment as devices provision.
-
Set environment variables. In Notehub, navigate to Fleet → Environment (or Device → Environment for a per-unit override). Add any of the variables below. The Notecard pulls them on its next inbound sync; no reflash, no truck roll. All are optional — firmware defaults apply when a variable is absent.
Variable Default Purpose sample_interval_sec120Seconds between sensor wakes. Min 30, max 3600. At 120 s the device takes 30 samples per hourly summary. summary_interval_min60Minutes between battery_summary.qoNotes. Changing this value also re-applieshub.seton the next wake so the Notecard's outbound session cadence stays in sync with the new summary interval.volt_min_v13.2Pack voltage (V) below which float_voltage_lowfires. Default calibrated for 12 V VRLA — the low end of the normal float window is approximately 13.2–13.5 V (2.20–2.25 V/cell). For 24 V systems set to ~26.4; for LFP systems, set chemistry-appropriate thresholds per the Chemistry-Specific Alert Notes in §7.volt_max_v14.8Pack voltage (V) above which float_voltage_highfires. Default calibrated for 12 V VRLA — typical float is 13.5–13.8 V; the default gives 1 V of margin above the absorption ceiling. For 24 V systems set to ~29.0; for LFP, see §8.float_current_hi_ma500Float current (mA) above which float_current_highfires. Default calibrated for 12 V VRLA — a healthy 100 Ah VRLA draws <50 mA at float; 500 mA is a clear anomaly signal. See the "Chemistry-Specific Alert Notes" sidebar in §8 for VRLA vs. LFP interpretation and commission a chemistry-appropriate threshold at installation.temp_alert_c40Pack surface temperature (°C) above which temp_highfires. VRLA batteries age roughly 2× faster for every 10 °C above 25 °C; 40 °C is the standard service-alert threshold.discharge_ma-200Current (mA) below which power_outagefires. −200 mA provides a clear margin above float-current noise while catching any sustained discharge within a single sample.usable_capacity_ah100Battery bank usable capacity (Ah) used for coulomb-counting SoC. Set this to the battery's nameplate capacity (or measured usable capacity from a full-discharge cycle). Required for meaningful soc_pctvalues — the firmware uses the default 100 Ah until overridden. For a 200 Ah bank set to200.soc_low_pct20SoC (%) below which soc_lowfires.soc_pct_init(unset) Initial SoC (%) to apply the first time this value is seen after a known-full charge. Set once after confirming the battery is fully charged (charger holding float voltage and current at minimum); the firmware tracks SoC from that baseline. To recalibrate: change the value to the new known SoC — the firmware detects the change and re-initializes on the next inbound sync. Leave unset (or set to −1) to defer commissioning;soc_pctin summary notes will carry−9999until a value is applied. -
Configure routes. Add one route targeting
battery_alert.qo; this is your real-time channel to an on-call queue, CMMS ticket, or alerting platform. Add a second route forbattery_summary.qoto a time-series analytics store where float-current trends can be visualized over weeks and months. The two Notefiles are separate by design so they can be fanned to different destinations with different urgencies and retention policies.
Expected events in Notehub
Within a minute of first power-on the Events tab should begin populating. Three event kinds matter:
-
_session.qo— automatic Notecard housekeeping on each cellular session; confirms the radio is reaching Notehub. -
battery_summary.qo— one persummary_interval_min.charge_ahis the net coulombs over the summary window: positive values during float operation, large negative values during site discharge.soc_pctis the running coulomb-counted state-of-charge estimate;−9999meanssoc_pct_inithas not yet been commissioned.curr_min_matracks the deepest discharge current (most-negative value) seen in the window;0.0means no discharge occurred. See the Quickstart section for full example JSON. -
battery_alert.qo— emitted only on a threshold trip or sensor fault, transmitted immediately. Thealertfield is one offloat_voltage_low,float_voltage_high,float_current_high,temp_high,power_outage,soc_low, orina228_unreachable. See the Quickstart section for example JSON.
7. Firmware Design
The firmware is split across four files in firmware/cabinet_battery_sentinel/:
| File | Contents |
|---|---|
cabinet_battery_sentinel.ino | Includes, global variable definitions, setup(), loop(), and the per-wake runCycle() |
cabinet_battery_sentinel_helpers.h | All #define constants (including PRODUCT_UID), SentinelState struct, extern globals, function prototypes |
cabinet_battery_sentinel_helpers.cpp | All helper function implementations |
cx_sleep.h | Host sleep: the card.attn request, STOP2 entry, and ATTN-pin wake (cxSleepBegin, cxSleepUntilAttn) |
7.1 Installing and flashing
Dependencies:
- Arduino core for STM32 — install via the Arduino Boards Manager (add the
index URL
https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.jsonunder File → Preferences → Additional Boards Manager URLs). The Notecarrier CX's host uses the Cygnet board definition (see build settings below). Blues Wireless Notecard(note-arduino): install via the Arduino Library Manager (arduino-cli lib install "Blues Wireless Notecard"). Check note-arduino releases and use the latest stable version.Adafruit INA228: install via the Arduino Library Manager (arduino-cli lib install "Adafruit INA228").Adafruit BusIO: required dependency of the INA228 library; install via Library Manager.STM32duino Low Power(and its dependencySTM32duino RTC) —stm32duino/STM32LowPower. Install via Library Manager. Provides the STOP2 entry and ATTN-pin wake used incx_sleep.h.
Build settings: the Notecarrier CX's host is the same STM32L433 as the Blues
Cygnet, so the board is Blues boards → Cygnet (pnum=CYGNET), with USB
support (if available) set to None (usb=none). With the USB CDC stack
enabled and no USB host attached (the Notecarrier CX USB-C switch in NC, or no
cable), the STM32's USB wakeup interrupt fires continuously and the host never
stays in STOP2. Debug output goes to the LPUART on the Notecarrier CX debug jack
instead — an ST-LINK V3 exposes it as a virtual COM port; see Serial Logging
With STLINK.
Before compiling, open cabinet_battery_sentinel_helpers.h and replace the
empty string on the #define PRODUCT_UID "" line with your Notehub ProjectUID.
All four source files in the sketch folder are compiled together automatically
by the Arduino build system.
Flashing via arduino-cli:
# Confirm the correct board name for your installed core version
arduino-cli board listall | grep -i cygnet
# Compile and upload (replace the board options and port with what the command
# above reports; usb=none disables USB CDC, which must be off for the host to sleep)
arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/cabinet_battery_sentinel/
arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none \
-p /dev/cu.usbmodem* \
firmware/cabinet_battery_sentinel/Flashing via Arduino IDE: open cabinet_battery_sentinel.ino, apply the
build settings above, select the correct port, and click Upload. With the
Notecarrier CX's DIP switch in HST, the host's DFU bootloader is reachable
over the USB-C port (hold BOOT, tap RST); alternatively upload over SWD with
an ST-LINK on the debug jack.
After upload, open a serial monitor at 115200 baud on the ST-LINK virtual
COM port. On each wake the firmware prints one or two [sentinel] lines — for
example:
[sentinel] volt=13.652V curr=12.5mA
[sentinel] temp=24.3CThen the host enters STOP2 for the sample interval and the monitor goes quiet.
INA228 FAIL or NTC open/shorted messages indicate a wiring problem on those
sensors. If the INA228 init fails, both INA228 readings (voltage and current)
are skipped for that sample; the summary accumulates only valid samples.
7.2 Modules
| Responsibility | Where |
|---|---|
Notecard config (hub.set, template, accelerometer) | notecardConfigure, defineTemplate (helpers.cpp) |
| Env-var fetch and range clamp | fetchEnvOverrides (helpers.cpp) |
| INA228 init and calibration (at power-up, retried on failure) | initINA228 (helpers.cpp) |
| Battery voltage and current reads | readBatteryVoltage, readBatteryCurrent (helpers.cpp) |
| NTC thermistor temperature read | readPackTempC (helpers.cpp) |
| Six battery-condition alert rules plus sensor-health fault, all with per-alert cooldowns | Inline in runCycle() (cabinet_battery_sentinel.ino) |
| INA228 persistent-fault remote notification | Inline in runCycle() + sendAlert |
| Hourly summary accumulation and emission | sendSummary (helpers.cpp) |
| Immediate-sync alert emission | sendAlert (helpers.cpp) |
Host sleep (STOP2 until the Notecard raises ATTN) | cx_sleep.h: cxSleepBegin, cxSleepUntilAttn |
| Application state (held in RAM across sleep cycles) | SentinelState |
7.3 Sensor reading strategy
INA228 (voltage and current). The Qwiic rail stays powered while the host
sleeps, so the INA228 is initialized once at power-up with begin() followed by
setShunt(0.015, 8.0) and keeps its calibration; the init is retried on later
wakes only if it failed. The calibration call programs the chip with the shunt
resistance and full-scale current. Single-point reads of readBusVoltage(),
readShuntVoltage(), and readCurrent() take well under 10 ms. Current is
signed at the firmware level: positive values mean the charger is supplying
current, negative values mean the battery is discharging. This sign convention
makes power_outage detection trivially simple: just a threshold comparison
against g_dischargeMa.
Battery-terminal voltage. With Battery(+) wired to INA228 V+ and the
load/charger bus wired to V−, readBusVoltage() returns the voltage at V− (load
side of the shunt), not the battery terminal. readBatteryVoltage() adds the
shunt voltage to recover the true battery-terminal voltage:
V_terminal = V_bus + readShuntVoltage() / 1000. The correction is ≤ 7.5 mV at
float currents up to 500 mA (negligible) and reaches ~48 mV at 3.2 A discharge —
material for voltage-alert accuracy and for the volt_v time series used in
float-voltage trending.
Current sign note. The INA228 is wired with Battery(+) on V+ and the load
bus on V−, so the chip internally reports positive raw current during
discharge (battery→load direction). readBatteryCurrent() negates the raw
readCurrent() result before returning it, so all downstream comparisons,
accumulations, and alert thresholds use the intuitive convention (positive =
charger present, negative = outage). The documented wiring in §4 and the
semantic descriptions throughout this README all refer to the post-negation
value.
The INA228 has an internal power register, but this design does not read it;
power_mw in the summary note is derived as voltAvg × currAvg in
sendSummary(), a window-average approximation, not a per-sample hardware
measurement.
NTC thermistor. A 16-sample average of 12-bit ADC readings
(analogReadResolution(12) on the STM32L433 host) reduces noise before applying
the β-equation: T = 1 / (1/T₀ + (1/β) × ln(R/R₀)). ADC readings within 50 mV
of the supply rails (indicating an open or shorted probe) return NAN and are
excluded from the temperature accumulator. A bad temperature reading never
silently biases the summary averages, and — because voltage, current, and
temperature each maintain their own independent sum and valid-sample count — a
failed thermistor probe never suppresses voltage or current accumulation, alert
evaluation, or the power_outage detection path.
Charge balance and SoC. Rather than relying on the INA228's hardware charge
accumulator (which resets whenever the chip loses power, for example when the
sentinel's own supply fails), the firmware accumulates charge in software:
chargeAh += (curr_mA / 1000) × (sample_interval_sec / 3600) each cycle. The
per-window result appears as charge_ah in the summary note: a small positive
value during normal float, a large negative value during a power outage.
charge_ah is a per-window delta, not state-of-charge. State-of-charge is
maintained separately in soc_pct: on each wake the same current-integration
delta is divided by usable_capacity_ah and added to the running SoC estimate,
which persists across sleep cycles in Notecard flash. soc_pct in the summary
note carries −9999 (SUMMARY_INVALID_SENTINEL) until the operator commissions a
starting SoC via soc_pct_init; see §6 for commissioning steps and §11 for
accuracy caveats.
7.4 Event payload design
battery_summary.qo is
template-backed,
giving it a fixed wire schema and a ~3–5× smaller on-wire footprint than
free-form JSON, material for a device that will send 24 notes per day for years.
battery_alert.qo is untemplated and uses sync:true for immediate delivery.
Field semantics:
power_mwis derived asvoltAvg × currAvg: a window-average approximation, not a per-sample hardware read from the INA228 power register.charge_ahis the net coulombs delivered to or drawn from the battery during the window: a per-window delta only, not running state-of-charge.soc_pctis the running state-of-charge estimate maintained across windows by integrating current against the commissionedusable_capacity_ahbaseline;−9999means not yet commissioned.curr_min_matracks the most-negative (deepest discharge) current seen in the window;0.0means no discharge occurred; a negative value (e.g.,−3200.0) means the battery was actively discharging at 3.2 A into the load.
Example JSON is shown in the Quickstart section above.
7.5 Low-power strategy
The host sleeps in STM32 STOP2 for the full 120-second sample interval: loop()
calls cxSleepUntilAttn() (in cx_sleep.h), which sends a
card.attn sleep
request for g_sampleSec seconds, waits for the Notecard to pull ATTN low,
and enters STOP2, where the STM32L433 draws ~1–2 µA with its RAM intact. When
the interval elapses the Notecard raises ATTN; the rising edge on D5 wakes
the host and execution resumes on the next line of loop(). That leaves the
Notecard idling at its own published ~8 µA. This is a Notecard-datasheet figure;
the actual current drawn from the 5 V supply during idle (as measured by Mojo on
the assembled device) will be higher, because it includes the Notecard's ~8 µA
plus the quiescent current of the DC-DC converter and Notecarrier CX regulators.
Because the host resumes in place rather than cold-booting, the SentinelState
struct simply lives in RAM — nothing is serialized to the Notecard, setup()
runs only once per power-up, and the rolling window accumulates correctly across
hundreds of sleep cycles per day. If the sleep request fails (for example, the
Notecard is still booting after a cold start) or ATTN never goes low (jumper
missing), loop() waits out the interval awake and tries again on the next
cycle, so the sample cadence is preserved either way.
The Notecard itself is set to periodic mode with outbound:60. Summary notes
queue in the on-device flash store and ship in a single cellular session once
per hour, keeping radio duty-cycle low. Alert notes with sync:true bypass the
outbound timer and wake the radio immediately. But because alert cooldowns
prevent re-firing for 30 minutes of wall-clock time (stored as remaining seconds
in SentinelState and decremented by the sample interval on each wake), even a
sustained fault doesn't generate continuous radio wakes, and the 30-minute
window holds regardless of whether sample_interval_sec is tuned to 30 s or 2
min.
7.6 Retry and error handling
- Notecard configuration. The initial
hub.setinnotecardConfigure()usessendRequestWithRetry(req, 5): a 5-second retry window covers the cold-boot I2C race where the STM32 comes up before the Notecard has finished initializing.note.templateregistration checks the boolean return value ofsendRequestand logs a debug message on failure.doFirstBoot()runs once fromsetup();hub.setand template registration are then retried fromrunCycle()on every wake until each is confirmed, so a transient fault at power-up cannot leave the device unassociated or untemplated. - Environment variable fetch.
fetchEnvOverrides()callsrequestAndResponseand inspects theerrfield before reading the body; a Notecard-side error (e.g. not yet associated with Notehub) returns early rather than silently leaving stale threshold values from a corrupted response. Thehub.setre-apply block inrunCycle()only updatesstate.lastSummaryMinaftersendRequestconfirms delivery, so a transient I2C fault doesn't desynchronize the recorded cadence from the Notecard's actual setting. - INA228 fault.
initINA228()returnsfalseon I2C NACK. Both INA228 readings (voltage and current) are set toNAN, excluded from their respective metric accumulators, and skipped in alert evaluation — the firmware continues to sleep rather than hanging on a sensor fault. Temperature accumulation and thetemp_highalert continue independently. A persistent INA228 failure is surfaced remotely: wheninitINA228()returnsfalseand thecoolInaFaultSeccooldown has expired, anina228_unreachablealert is emitted tobattery_alert.qo(rate-limited to once per 30 minutes so a sustained hardware fault does not flood the notefile). When the INA228 is unreachable for an entire summary window,sendSummary()still emits the note — all INA228 fields carrySUMMARY_INVALID_SENTINELandsamplesis 0 — so Notehub shows a visible fault window rather than a silent gap. - NTC fault. ADC readings within 50 mV of the supply rails return
NANand are excluded from temperature accumulation. When only the thermistor is faulted (INA228 data is still valid), the summary is emitted normally withSUMMARY_INVALID_SENTINEL(−9999) in thetemp_candtemp_max_cfields so downstream analytics can distinguish "sensor failed" from a true near-zero reading. When the INA228 is also unreachable for the entire window,sendSummary()still emits — all INA228 fields carrySUMMARY_INVALID_SENTINELandsamplesis 0; theina228_unreachablealert provides the immediate notification while the sentinel-filled summary preserves time-series continuity. - Note delivery.
sendSummary()andsendAlert()each retrynote.addup to three times with a 500 ms delay between attempts.sendSummary()returns a boolean; metric accumulators and the window elapsed timer are only reset after a confirmed successful delivery; a transient Notecard I2C fault preserves the window data so the next wake retries with the data intact rather than losing the window silently. - Alert gating.
float_voltage_low,float_voltage_high, andfloat_current_highare suppressed during active battery discharge (curr < discharge_ma) to prevent misleading float-fault alerts during legitimate power-outage events.float_current_highis additionally suppressed for 30 minutes after the last discharge sample (thepostDischargeSecsettling window in the state struct) so normal bulk-recharge current following an outage recovery is not misclassified as elevated float current. - Env var range clamping.
g_sampleSecis clamped to [30, 3600] andg_summaryMinto [5, 1440] so a misconfigured variable cannot produce absurd sleep intervals or an unreachable summary window. Whensample_interval_secchanges mid-window,windowElapsedSecis reset so the next summary covers exactly the newly configured interval rather than an unintended hybrid duration. - All six battery-condition rules and the sensor-health alert use separate cooldown counters and fire independently. A battery that is simultaneously low-voltage and overtemperature fires both alerts; neither suppresses the other.
7.7 Key code snippet 1 — template registration
The template registers the battery_summary.qo schema as a fixed-length wire
record, compressing every Note to a fixed byte count for ~3–5× smaller
over-the-air footprint. Format codes use the notation <type><decimals>:
14.2= 4-byte IEEE-754 float, encoded to 2 decimal places on the wire (e.g.13.65V becomes two bytes instead of the full float)14.1= 4-byte float, 1 decimal place (e.g.,12.5mA)14.3= 4-byte float, 3 decimal places (e.g.,0.0125Ah)12= 2-byte signed integer (e.g.,30samples)
Any subsequent note.add with a body that doesn't match this schema is rejected
by the Notecard, catching firmware mistakes at the device rather than corrupting
the downstream time series.
J *req = notecard.newRequest("note.template");
JAddStringToObject(req, "file", "battery_summary.qo");
JAddNumberToObject(req, "port", 50);
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "volt_v", 14.2); // battery-terminal voltage (V)
JAddNumberToObject(body, "curr_ma", 14.1);
JAddNumberToObject(body, "power_mw", 14.1);
JAddNumberToObject(body, "charge_ah", 14.3); // net coulombs this window (per-window delta)
JAddNumberToObject(body, "soc_pct", 14.1); // running SoC estimate (%); -9999 = not commissioned
JAddNumberToObject(body, "temp_c", 14.1);
JAddNumberToObject(body, "volt_min_v", 14.2);
JAddNumberToObject(body, "curr_min_ma", 14.1);
JAddNumberToObject(body, "temp_max_c", 14.1);
JAddNumberToObject(body, "samples", 12);
notecard.sendRequest(req);7.8 Key code snippet 2 — immediate power-outage alert
sync:true tells the Notecard to wake the radio immediately instead of waiting
for the next outbound window. An operator can be notified within one sample
interval (120 seconds at default settings) plus cellular session-establishment
time (~15–60 seconds typical on LTE Cat-1 bis) after a current reversal crosses
the discharge_ma threshold, so roughly two to three minutes end-to-end from
the moment mains fails to the alert landing in Notehub at default settings.
Reduce sample_interval_sec via env var if a shorter detection window is
needed.
J *req = notecard.newRequest("note.add");
JAddStringToObject(req, "file", "battery_alert.qo");
JAddBoolToObject(req, "sync", true);
J *body = JAddObjectToObject(req, "body");
JAddStringToObject(body, "alert", "power_outage");
JAddNumberToObject(body, "volt_v", volt);
JAddNumberToObject(body, "curr_ma", curr);
JAddNumberToObject(body, "temp_c", temp);
notecard.sendRequest(req);7.9 Key code snippet 3 — sleeping until the Notecard raises ATTN
cxSleepUntilAttn() asks the Notecard to hold ATTN low for exactly
g_sampleSec seconds, confirms the pin has gone low, and puts the STM32 into
STOP2. The host stays alive, so the card.attn call is a request whose result
is checked (with retries, to cover the Notecard still booting after a cold
start) rather than a fire-and-forget command. When the Notecard raises ATTN,
loop() runs the next cycle with SentinelState exactly as it was left:
rolling averages, window extremes, alert cooldowns, and charge balance all
survive intact in RAM.
// In setup() — arm the ATTN wake once
cxSleepBegin();
// In loop() — one sample cycle, then sleep until ATTN rises
runCycle();
if (!cxSleepUntilAttn(notecard, g_sampleSec, NULL, &dbgSerial)) {
// Notecard not ready, or ATTN never went low (check the ATTN -> D5 jumper).
// Keep the sample cadence and try again next cycle.
delay(g_sampleSec * 1000UL);
}8. Data Flow
Collected every sample_interval_sec (default 120 seconds): pack voltage in
V, float/discharge current in mA, and pack surface temperature in °C.
Window-average power (mW) is derived as voltAvg × currAvg at summary time, not
sampled directly from the INA228 power register.
Accumulated in SentinelState across sleep cycles: rolling sums for window
averages, the minimum voltage seen at a sample point (useful for detecting
voltage sag, Note that sags entirely contained between two consecutive 2-minute
samples will not be captured), the most-negative current (captures peak
discharge rate during a power event), the maximum temperature, and the net
charge balance in amp-hours.
Transmitted:
battery_summary.qo— one record persummary_interval_min(default 24 per day), template-encoded. Window averages plus extremes. A healthy battery at float shows a small positivecharge_ahproportional to float current and window duration (e.g., 0.0125 Ah for 12.5 mA average over one hour); a battery that spent time discharging shows a large negativecharge_ah.battery_alert.qo— emitted immediately on a threshold trip, with per-alert cooldowns preventing re-firing for approximately 30 minutes per alert type.
Routed: Notehub fans both Notefiles to configured routes. Natural split:
battery_alert.qo → real-time on-call, CMMS, or SMS gateway;
battery_summary.qo → time-series database for multi-week float-current trend
analysis.
Alert triggers — six battery-condition rules plus one sensor-health alert:
| Alert | Condition | Diagnostic meaning |
|---|---|---|
float_voltage_low | Voltage < volt_min_v while charger present (curr ≥ discharge_ma) | Charger not supplying enough current, failed cell, or battery unable to reach float voltage under load. Suppressed during active discharge so a legitimate voltage sag while running on battery does not generate a spurious float-fault alert. |
float_voltage_high | Voltage > volt_max_v while charger present (curr ≥ discharge_ma) | Charger overvoltage fault; in VRLA, accelerates plate corrosion and electrolyte loss via gassing. Suppressed during active discharge for the same reason as float_voltage_low. |
float_current_high | Current > float_current_hi_ma while charger present and post-discharge settling window expired | Elevated float current. See "Chemistry-Specific Alert Notes" sidebar below. Suppressed during active discharge and for 30 minutes after the last discharge sample so normal bulk-recharge current following an outage recovery is not misclassified as float degradation. |
temp_high | Temperature > temp_alert_c | Elevated battery case temperature. A single surface thermistor cannot identify the heat source or confirm specific failure modes — it indicates the battery, charger, or enclosure is running abnormally warm. Sustained elevated temperature accelerates battery aging. Inspect charger output, enclosure ventilation, and battery condition. |
power_outage | Current < discharge_ma | Site is running on battery. The event the whole system was built to detect — and to survive long enough to report. |
soc_low | soc_pct < soc_low_pct and SoC has been commissioned | State-of-charge has fallen below the operator-configured threshold. Only fires after soc_pct_init has been set; soc_pct is included automatically in the alert note. Inspect whether a recent discharge event was deeper or longer than expected — a battery that drives SoC low during a brief outage has less reserve than its nameplate rating suggests. |
ina228_unreachable | INA228 begin() fails on I2C NACK | The primary current and voltage sensor is not responding. All INA228-derived telemetry is blind until the fault clears. Rate-limited to once per 30 minutes. Inspect Qwiic wiring, I2C address (A0/A1 jumpers), and 3.3 V supply. |
Chemistry-Specific Alert Notes
VRLA batteries: Elevated float current is a reliable early indicator of
sulfation, internal resistance growth, or approaching end-of-life — typically
weeks before a battery fails a load test. This is a pack-level aggregate
signal indicating the string as a whole requires more continuous charge
current than a healthy pack, consistent with one or more degraded cells, but
does not identify which cell or confirm per-cell imbalance. Default thresholds
are calibrated for 12 V VRLA (volt_min_v=13.2, volt_max_v=14.8,
float_current_hi_ma=500). For 24 V VRLA, scale voltage thresholds
proportionally (volt_min_v≈26.4, volt_max_v≈29.0).
LFP (lithium iron phosphate) batteries: Elevated float current does not
indicate cell degradation — LFP packs do not sulfate. On an LFP pack, elevated
float current typically signals a charger or BMS configuration fault instead.
Voltage monitoring, power_outage detection, and temp_high remain fully
applicable. Do not commission LFP systems on default VRLA thresholds; set
volt_min_v, volt_max_v, and float_current_hi_ma to values calibrated for
your specific LFP charger curve (typically narrower than VRLA — e.g., float
~13.5–13.8 V, absorption ~14.6 V for most LFP). Commission thresholds at
installation after observing your charger's normal operating range.
9. Validation and Testing
Expected steady-state behavior. A healthy backup battery generates
approximately one battery_summary.qo per hour and zero battery_alert.qo
events. The _session.qo session events confirm cellular connectivity is
healthy. Expect a brief flurry of battery_alert.qo events during the first few
days while thresholds are tuned to the specific battery and charger on site;
watch one week of hourly summaries before treating the baseline as calibrated.
Simulating a power outage. During bench bring-up the simplest test is to
temporarily lower discharge_ma to −5.0 via a Notehub environment variable.
On the next inbound sync the Notecard pulls the new value; on the next sample,
any measurement more negative than −5 mA fires power_outage. Restore the
threshold after the test. Alternatively, physically disconnect the charger while
monitoring the bench rig; the first sample after current reverses will emit an
alert.
Using Mojo to validate power behavior. The Notecard's published datasheet current figures — see the low-power design guide for authoritative numbers across SKUs and modes:
| Phase | Current @ 5 V |
|---|---|
| Idle baseline (host in STOP2, radio off) | ~10 µA (Notecard ~8 µA + STM32L433 in STOP2 ~1–2 µA) |
| Host wake, sensor read (STM32L433 + INA228 + ADC) | ~15–20 mA for <1 s |
| Cellular session, LTE Cat-1 bis (one queued note) | ~200–300 mA average, 15–30 s |
Whole-device power profile. The figures above for Notecard idle and cellular
are datasheet specs. On the assembled device, the idle baseline (host in STOP2
waiting for ATTN, radio idle) measured on a 5 V power rail includes the
Notecard's ~8 µA and the host's ~1–2 µA plus the DC-DC converter's quiescent
draw (~2 mA for RECOM R-78E5.0-1.0 or Pololu D24V50F5) plus Notecarrier CX
regulator quiescent — typically 2–5 mA total at no load. Use the trace
shape — a 2-minute wake blip (~15–20 mA for <1 s), an hourly burst
(~200–300 mA for 15–30 s), and a flat floor in between — to confirm correct
sleep/wake behavior. The Mojo is spliced on the 5 V power rail and measures the
entire assembled device; focus on a flat baseline at the expected quiescent
level for your DC-DC converter rather than chasing sub-milliamp absolutes.
A useful Mojo trace pattern for a healthy unit:
- Blip every 2 minutes (<1 s at roughly 15–20 mA) — host awake reading sensors
- Sustained burst once per hour (15–30 s at 200–300 mA) — Notecard cellular session
- Flat baseline between ~2–5 mA — host in STOP2, Notecard radio idle, floor dominated by the DC-DC converter's quiescent current. The RECOM R-78E5.0-1.0 draws approximately 2 mA at no load; the Pololu D24V50F5 is in the same range. Focus on a flat floor at the expected level for your converter rather than chasing a sub-milliamp absolute figure.
Baseline measurement conditions. For the idle-current floor to reflect deployment conditions, power the device through the DC-DC module into the +VBAT header with no USB cable attached. A connected USB cable can prevent the Notecard from entering its lowest-power state, producing a higher and misleading baseline reading; the host itself only stays in STOP2 when built with USB support set to None.
Use the Mojo to measure whole-device energy over a full 24-hour cycle on your specific hardware; actual consumption depends on signal conditions, LiPo charge efficiency, and the DC-DC module's light-load efficiency.
Trace anomalies to diagnose:
- Continuous 80–150 mA baseline: the host is not entering STOP2 — check the
ATTN→D5jumper (see §5) and confirm the firmware was built with theusb=noneboard option (USB support set to None). - Hourly bursts exceeding 60 seconds: the radio is struggling on weak signal. Move the cellular antenna or improve antenna routing out of the cabinet.
- No blips, no bursts: the Notecard is not reaching Notehub. Check
PRODUCT_UIDmatches the Notehub project exactly and verify cellular coverage at the installation site.
Mojo is not required in deployed hardware; it is a bench bring-up and regression tool. Once a firmware version passes the trace check, deployed units don't need it.
A Notecarrier CX and a Cell+WiFi Notecard, paired with a 20-bit precision current monitor and a surface thermistor, turn a passive backup battery into a continuously-monitored asset that reports float conditions every two minutes, tracks state-of-charge via coulomb counting, trends float current over months as an early indicator of VRLA capacity degradation, and pages an operator within one sample interval of the site going dark. The same hardware and firmware run on 12 V roadside cabinet batteries and 24 V industrial UPS banks. The cellular uplink is independent of every piece of equipment the battery protects — which is the whole point. When the cabinet's own LTE radio shuts down because the mains failed and the battery turned out to be half-sulfated and incapable of holding the load, the sentinel is still running — riding on that failing battery through the DC-DC converter, capturing the discharge curve as the voltage collapses, and then switching to the onboard LiPo for the final reporting tail as the bus drops out.
For operations teams maintaining a fleet of roadside or remote-cabinet
batteries, the float-current trend in battery_summary.qo is the early-warning
signal that pays for the deployment many times over: elevated float current
weeks before a battery fails a load test is the difference between a scheduled
swap during a maintenance window and an emergency truck roll at 2 AM. Commission
usable_capacity_ah and soc_pct_init at installation to unlock soc_pct
trending alongside float current — two complementary leading indicators. Route
battery_alert.qo into your existing CMMS and battery_summary.qo into a
time-series database, and within a service season you will have a corpus of
leading indicators to compare against actual failures — the foundation of a
proactive battery-management program built on real field data.
10. Troubleshooting
Most bring-up failures fall into a small set of categories — wiring polarity on
the INA228 shunt, a mismatched PRODUCT_UID, marginal cellular coverage inside
a metal cabinet, or a misconfigured threshold producing alerts that look like
sensor faults. Work the table below top-to-bottom: confirm the Notecard is
reaching Notehub before chasing sensor symptoms, then validate the INA228 reads,
then evaluate alert behavior.
| Symptom | Likely Cause | What to Check |
|---|---|---|
Device never appears on the Notehub Devices tab; no _session.qo events. | PRODUCT_UID is empty or does not exactly match the Notehub project; cellular antenna is inside a sealed metal cabinet; SIM not yet activated on its first session. | Re-verify PRODUCT_UID in cabinet_battery_sentinel_helpers.h matches the Notehub project. Move the test unit outdoors or attach a known-good external antenna. Confirm {"req":"card.status"} in the In-Browser Terminal reports a connected modem. Allow up to 5 minutes on a cold start. |
_session.qo arrives but battery_summary.qo never does. | First summary_interval_min window has not elapsed yet; INA228 init is failing every wake so all samples are rejected; firmware is not running. | Watch the debug serial output (ST-LINK virtual COM port) at 115200 baud. Each wake should print [sentinel] volt=… and [sentinel] temp=…. If you see only INA228 FAIL, the chip is not responding — see the next row. |
[sentinel] INA228 FAIL on every wake, or ina228_unreachable alert fires repeatedly. | Qwiic cable not seated, INA228 not powered, or I2C address-select jumpers conflicting with the firmware default (0x40). | Reseat the Qwiic cable on both ends. Confirm the breakout's onboard 3.3 V LED is lit. Check the A0/A1 jumpers on the INA228 breakout — leave them at the factory default unless you have changed INA228_I2C_ADDRESS in firmware. |
volt_v reads 0, a flat clamped value, or wildly negative numbers. | Battery (+) is not connected to the INA228 V+ pad, the load bus is mis-wired to ground rather than to V-, or the bus voltage exceeds the INA228 common-mode limit (85 V). | Probe V+ to circuit ground with a DMM and confirm it sits at the battery's terminal voltage. Verify V- is bonded to the load bus, not to the battery negative. Confirm the bus is within 12 V or 24 V VRLA/LFP range as covered by §4. |
curr_ma is stuck at 0.0 regardless of charger state. | Shunt is shorted across by a parallel conductor (the load bus is bonded directly to the battery positive instead of routing through the INA228), or the shunt-resistance constant in firmware does not match the installed shunt. | Remove parallel paths so the only conductor between battery (+) and the load bus passes through the INA228. Confirm INA228_SHUNT_OHMS matches the breakout's onboard shunt (15 mΩ) or the external shunt you installed. |
Frequent power_outage alerts on a site that has stable mains power. | Shunt wiring is reversed (battery on V-, load on V+), so charge current reads as negative; or discharge_ma threshold is too close to zero and ordinary float-current noise crosses it. | Verify shunt polarity per §5: battery (+) to V+, load bus (+) to V-. The current sign convention is documented in §7.3 — positive means charger present, negative means discharging. If polarity is correct, raise the magnitude of discharge_ma (for example to -500) via Notehub environment variable. |
temp_c reads −9999.0 while voltage and current readings look fine. | NTC thermistor probe is open, shorted, or the 10 kΩ divider resistor is missing. | Probe A0 with a DMM at room temperature — it should sit near 1.65 V with a 10 kΩ NTC and a 10 kΩ pull-up to 3V3. Inspect the divider wiring against §5. |
battery_summary.qo fields all carry −9999 and samples is 0. | INA228 was unreachable for the entire window; the sentinel-filled summary preserves time-series continuity. | Treat this as a hard sensor fault — see the INA228 row above. The simultaneous ina228_unreachable alert in battery_alert.qo is the immediate notification. |
| Cellular session takes more than 60 seconds, or hourly bursts run for several minutes on the Mojo trace. | Marginal LTE Cat-1 bis signal inside the cabinet; rubber-duck or internal antenna in a metal enclosure. | Reroute the antenna to the cabinet exterior using the SMA bulkhead pigtail described in §4. Re-test with the antenna in clear sky view. |
| The sentinel itself runs flat after a few hours on the bench. | The host is not entering STOP2 (missing ATTN→D5 jumper, or the firmware was built with USB CDC enabled), USB power is connected at the same time as the +VBAT header, or the LiPo is undersized for the chosen sample interval. | Check the jumper and the usb=none build option (see §7.1). Disconnect the USB cable for the baseline measurement (see §9). Confirm the 2000 mAh LiPo is plugged into the JST connector. If the sample interval has been pushed below 30 seconds, expect proportionally higher average current. |
If a symptom does not appear above, post a description (with the relevant
[sentinel] serial output and the trailing _session.qo payload) on the Blues
community forum.
11. Limitations and Next Steps
This design targets one pack on one bus and the pack-level signal that catches the most common cabinet failure modes. Cell-level diagnostics, multi-bus shelters, and full state-of-health modeling are deliberate scope choices left to companion designs — the goal here is to get the simplest defensible cabinet sentinel into the field, then layer on richer telemetry where the site warrants it.
Simplified for the POC
The simplifications below are deliberate scope choices — each is a place where a production deployment will reach for another channel, another sensor, or a richer state estimator once a specific site demands it.
Single pack, single bus. The firmware monitors one pack on one positive-referenced DC bus. Cabinets with a primary bank and a parallel reserve, or multi-bus telecom shelters with separate −48 V plant and 12 V auxiliary loads, need one sentinel per bus or a multi-channel front end.
One INA228 with a single shunt. The default 15 mΩ onboard shunt covers up to
roughly 8 A continuous. Higher-current installations require an external busbar
shunt (see §4 and §5); the firmware exposes INA228_SHUNT_OHMS and
INA228_MAX_CURRENT_A for that case but does not auto-detect.
Pack-level signals only — no cell-level visibility. Per-cell voltage and per-cell imbalance detection are out of scope. Float-current trending identifies that the string as a whole is degrading, not which cell is failing.
No state-of-health (SoH) modeling. SoC is tracked by coulomb counting
against a commissioned usable_capacity_ah, but the firmware does not estimate
capacity fade over time. Re-commissioning usable_capacity_ah after a measured
discharge cycle is a manual step.
No temperature compensation on voltage thresholds. VRLA float voltage drifts
with battery temperature (typically −3 to −5 mV per cell per °C). The fixed
volt_min_v and volt_max_v thresholds do not adjust automatically; in a
cabinet that swings 30 °C across seasons, set thresholds with margin or split
the fleet by climate zone.
One thermistor on the case surface. A single surface probe near the geometric center of the pack flags gross thermal events but cannot distinguish a hot cell from charger-induced bulk heating, and it cannot resolve internal cell temperature.
OCV-based SoC is approximate, especially under load. Coulomb counting against the nameplate capacity is the only state estimator; the firmware does not blend in OCV at rest, does not detect a full-charge event automatically, and will drift between manual recalibrations.
No inbound command channel. Operators tune behavior via Notehub environment
variables — there is no battery_command.qi for fleet-triggered actions such as
"clear alert cooldowns" or "reset SoC after a pack swap."
Production Next Steps
Once a real cabinet fleet is running the basic sentinel, the following extensions are the natural progression — roughly from the most immediately useful to the most integration-dependent.
Add a temperature probe on the pack interior or terminal post for a more accurate read of the chemistry's thermal state, and apply temperature compensation to the float-voltage thresholds and to the coulomb-counting charge efficiency factor.
Blend OCV at rest with coulomb counting for a self-correcting SoC estimate that recovers from drift after a long quiescent period, and add an automatic full-charge detector to re-anchor SoC to 100 percent without operator intervention.
Layer in cell-level monitoring via a dedicated cell-monitor IC (TI BQ76940 or equivalent), CAN, or Bluetooth into a pack-resident BMS. A cell-level channel transforms the float-current signal from a string-level aggregate into a per-cell diagnostic.
Integrate with the site's existing BMS when one is present: many telecom-grade VRLA strings ship with a CAN or RS-485 BMS that already exposes per-cell voltages and SoH. Adding a CAN front end and a vendor-specific frame parser turns this design into a redundant cellular uplink for that BMS rather than a parallel measurement.
Aggregate across a fleet in a downstream time-series store. Float-current trends per cabinet, ranked against the fleet median, surface the outlier cabinets weeks before any one of them trips an absolute threshold.
Deploy Notecard Outboard DFU so threshold recipes, OCV blends, and chemistry-specific calibrations can be pushed across the fleet without a truck roll.
Add an inbound battery_command.qi Notefile for fleet operators to reset
SoH accumulators after a pack swap, clear alert cooldowns following a planned
maintenance visit, or trigger an immediate ad-hoc sample-and-report.
12. Summary
The traffic engineer (or telecom site owner, or industrial RTU operator) now has the one signal that matters: a creeping float-current trend weeks before a battery would have failed under load, and an immediate page within one sample interval if the site does go dark. Because the Notecard rides on its own cellular session, the "battery is failing" alert reaches Notehub even when the site's own modem and gateway have already lost power — and the LiPo reporting tail captures the most diagnostically valuable minutes of a battery's life: the moments after mains drops and before the bus collapses. One avoided emergency truck roll, or one intersection that stays up through a regional outage, pays for the sentinel many times over.
The same architecture and firmware — with the threshold recipe and shunt sized
per chemistry and per current range — carries from a 12 V VRLA roadside cabinet
to a 24 V LFP industrial UPS bank, and from one cabinet to a fleet once
battery_summary.qo is routed into a time-series store and ranked against the
fleet baseline.
On This Page
- 1. Project Overview
- 2. System Architecture
- 3. Technical Summary
- 4. Hardware Requirements
- 5. Wiring and Assembly
- 6. Notehub Setup
- 7. Firmware Design
- 7.1 Installing and flashing
- 7.2 Modules
- 7.3 Sensor reading strategy
- 7.4 Event payload design
- 7.5 Low-power strategy
- 7.6 Retry and error handling
- 7.7 Key code snippet 1 — template registration
- 7.8 Key code snippet 2 — immediate power-outage alert
- 7.9 Key code snippet 3 — sleeping until the Notecard raises ATTN
- 8. Data Flow
- 9. Validation and Testing
- 10. Troubleshooting
- 11. Limitations and Next Steps
- 12. Summary