Multi-Site Walk-In Cooler Energy & Setpoint Monitor

This reference application is here to give you ideas and help you get started quickly. It's built on specific hardware that may not match yours, so focus on the sections that fit your use case. If you'd like to talk through your project and whether it's a good fit for Blues, reach out.
Building with an AI assistant? Point it at this accelerator's firmware in firmware/cooler_monitor/ so it can learn how the device works and help you adapt it to your hardware.
This project is a cellular energy savings reference design for walk-in coolers. It gives corporate operations teams a live view of the temperature, compressor runtime, and door behavior of every walk-in cooler in their fleet, and it never touches a single franchisee's or operator's WiFi network.
1. Project Overview
The problem. For a chain QSR (quick-service restaurant), convenience store, or grocery operator, the walk-in cooler is one of the most expensive assets in a store and one of the least visible. Per location, energy costs more than anything except labor, and a big share of that energy budget runs through the compressor of one or more walk-in boxes. Operators who can see compressor runtime, door discipline, and temperature trends across their fleet can make a real dent in energy spend. A door held open five minutes too long during a busy lunch rush costs real money in wasted refrigeration. Then there's the compressor that runs six hours a day instead of four because box air temperature has quietly drifted 2 °F above the corporate temperature target. At scale, that costs even more.
The harder problem is that an operator running 800 locations has 800 different network environments. Franchisees are on consumer ISPs, independent operators run POS (point-of-sale) systems their IT vendors won't let anyone touch, and convenience stores have back-office networks that rule out any new guest devices. Getting corporate visibility through all of that is what keeps most energy-monitoring pilots from turning into fleet-wide programs. The pilot sites get instrumented, and then the rollout stalls at 50.
This device is built to get past that barrier. It's a single cellular-connected
SKU that doesn't need an IT ticket. Stick a temperature probe inside the box,
clamp a split-core current transformer on the compressor hot leg, and mount a
magnetic reed switch on the door. You get a per-unit compressor energy proxy,
door-open events, and temperature-to-target deviation, all delivered to the
Blues Notehub cloud service and routed wherever
corporate needs them. The energy proxy is compressor apparent kWh per summary
window, and per-day totals come from summing kwh_window records downstream.
Why Notecard. Corporate energy management can't touch 800 independent-operator or franchisee WiFi networks, because each one would be its own ticket. A cellular Notecard is one SKU the field can plug in without asking anyone for a WiFi password. That's what separates a program that deploys at fleet scale from one that never leaves the pilot. Cellular also keeps the energy-monitoring data stream entirely off the POS network. That matters for network security, and it matters because POS downtime is the one side effect of an energy program that nobody will accept. Notecard Cell+WiFi keeps WiFi as an opportunistic fallback for the occasional site that can offer it, and the deployment model stays cellular-first.
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.
Deployment scenario. A small weatherproof enclosure mounts in or near the cooler's refrigeration cabinet or mechanical room. It runs off the cooler's dedicated circuit, which is typically 120VAC for single-phase residential-style units, though many commercial condensing units run on 208/240VAC (see Limitations). Three sensor leads run into the box. The first is a waterproof DS18B20 temperature probe in open box air at mid-box height or along the return-air path, where it reads representative box air temperature away from the evaporator and door. A split-core CT clamps onto one hot leg of the compressor's dedicated circuit, and a two-wire reed switch mounts on the door frame with a magnet on the door itself. You don't need the OEM's cooperation, any changes to the cooler, or any network integration.
2. System Architecture
Device-side responsibilities. Inside the cooler's mechanical-room enclosure,
the Notecarrier CX's STM32 host wakes every sample_interval_sec seconds
(default 60), reads all three sensors, updates the per-window accumulators (kWh
proxy, compressor runtime, door open seconds and event count), and runs the two
alert rules. Then it either queues a summary
Note for the next window or goes straight back
to sleep. Between wakes the host sleeps in STM32 STOP2 (a few µA) using
card.attn. The
Notecard holds its ATTN pin low for the interval and raises it to wake the
host when the next sample is due.
Notecard responsibilities. The Notecard handles whatever the host queues.
Notes wait in the on-device queue until the
hub.set outbound
cadence (60 minutes by default) opens a cellular (or opportunistic WiFi) session
and flushes them in one batch. Anything tagged sync:true skips the queue, and
the radio comes up right away. On every inbound sync, the Notecard also pulls
environment
variables
from Notehub, so you can tune setpoint targets, alert timers, and cadences from
a browser without a firmware update or a field visit.
Notehub responsibilities. Events travel over the Notecard's embedded global
SIM to Blues Notehub, which ingests and stores every one
and runs the project-level routes to your cloud application of choice.
Summaries and alerts go to separate JSON-based
Notefiles on purpose. cooler_summary.qo
carries the hourly telemetry that flows to BI or a long-term store, and
cooler_alert.qo carries the immediate notifications that need to land on a
phone or a Slack channel. The routes need no filter logic, since they're just
two streams pointed at the right destinations.
Fleets and Smart
Fleets make it easy to
set a corporate temperature target per banner or region and still override it
for the one flagship that runs tighter.
Routing to the cloud. Notehub can route to HTTP, MQTT, AWS, Azure, GCP, Snowflake, and other destinations. Route configuration depends on your project, and this one doesn't ship a downstream endpoint. The Notehub routing docs cover setup.
3. Technical Summary
First Event in 30 Minutes
-
Assemble the hardware (Section 4–5): Notecarrier CX + Notecard Cell+WiFi, DS18B20 temperature probe, SCT-013-030 current transformer, reed switch door sensor, and 5V/2A power supply in a NEMA 4X enclosure.
-
Create a Notehub project and copy your ProductUID.
-
Flash the firmware:
git clone https://github.com/blues/app-accelerators.git cd app-accelerators/54-multi-site-walk-in-cooler-energy-setpoint-monitor/firmware/cooler_monitor # Edit cooler_monitor_helpers.h: paste your ProductUID into the PRODUCT_UID constant arduino-cli core install "STMicroelectronics:stm32" arduino-cli lib install "Blues Wireless Notecard" "STM32duino Low Power" "STM32duino RTC" "OneWire" "DallasTemperature" arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none . arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none -p /dev/ttyACM0 .The
usb=noneboard option disables the USB CDC stack, which has to be off for the host to sleep (see Section 7).sketch.yamlin that directory sets the same board options, soarduino-cli compilewithout-bpicks them up too. -
Power the device, then in Notehub open your project → Events tab. The first
cooler_summary.qoevent (hourly telemetry) arrives once the first 60-minute summary window closes and the next outbound sync runs, so allow up to about two hours.cooler_alert.qo(threshold events) only shows up when an alert fires, which step 6 forces. A typical summary event looks like this:{ "temp_f": 35.4, "setpoint_f": 35.0, "compressor_amps": 3.5, "compressor_run_min": 32.0, "door_opens": 5, "door_open_sec": 300, "kwh_window": 0.42, "window_sec": 3600 } -
Set threshold environment variables in Notehub (Fleet → Environment → click a row, edit the JSON):
temp_alert_f: 40 (fires when box temp exceeds this, default 40 °F)door_open_alert_sec: 300 (fires when the door stays open continuously for this many seconds, default 5 minutes)- Other optional vars:
sample_interval_sec,summary_interval_min,temp_setpoint_f,compressor_on_amps,volts_nominal
-
Trigger a test alert: Open the cooler door, leave it open for >5 minutes, and wait for the next sample cycle. A
door_open_longalert shows up incooler_alert.qowithsync:true(immediate delivery, not queued).
What You'll Have When You're Done
When you've finished, you'll have deployed a complete cellular-connected walk-in cooler monitoring system made up of:
-
Three non-invasive sensors mounted on the cooler (temperature probe in the box, current transformer clamp on the compressor hot leg, magnetic reed switch on the door). They need no electrical changes and no cooperation from the cooler's OEM.
-
A local enclosure (Notecarrier CX with onboard STM32 host, Notecard Cell+WiFi, power supply, and bias/pull-up circuits) that samples all three sensors every 60 seconds and automatically sleeps in STOP2 between samples.
-
Notehub as your cloud backend, storing and routing two streams of data:
- Telemetry: One summary event per hour per cooler, with averages (temperature, compressor amps) and totals (runtime minutes, door-open events, apparent kWh, window duration).
- Alerts: Immediate notifications whenever the door is held open > 5 minutes or box temperature exceeds 40 °F, delivered through Notehub routes to your ops/facilities team.
-
No WiFi required. Cellular connectivity removes the IT ticket bottleneck that has stopped energy programs at hundreds of multi-location operators. It works on franchisee sites, POS-restricted networks, and anywhere there's a cell signal.
-
Configuration without reflashing. You can tune threshold temperatures, alert timers, and sample/summary cadences with Notehub environment variables, and changes take effect on the device's next inbound sync.
Here's a sample Note this device emits:
{
"file": "cooler_summary.qo",
"body": {
"temp_f": 35.4,
"setpoint_f": 35.0,
"compressor_amps": 3.5,
"compressor_run_min": 32.0,
"door_opens": 5,
"door_open_sec": 300,
"kwh_window": 0.42,
"window_sec": 3600
}
}4. Hardware Requirements
| Part | Qty | Rationale |
|---|---|---|
| Notecarrier CX | 1 | Integrated carrier with an onboard STM32L433 host, so this three-sensor combination doesn't need a separate MCU. The host sleeps in STOP2 between samples until the Notecard's ATTN pin wakes it. |
| Notecard Cell+WiFi (MBGLW) (datasheet) | 1 | Cellular-first connectivity that skips site WiFi entirely, with WiFi fallback for the occasional cooperative site. |
| Blues Mojo | 1 | Inline coulomb counter for checking sleep/wake current profiles on the bench before field deployment. |
| Adafruit Waterproof DS18B20 Temperature Sensor (product 381) | 1 | Calibrated digital 1-Wire temperature probe (±0.5 °C accuracy) in a stainless steel enclosure, built for wet food-service environments where a bare thermistor would corrode. |
| 4.7 kΩ resistor, ¼W | 1 | Required pull-up for the DS18B20 1-Wire data line. Without it, the bus hangs. |
| YHDC SCT-013-030 split-core CT, 30A / 1V RMS (the voltage-output version, with a built-in burden resistor, so you don't add one) | 1 | Non-invasive current measurement that clamps over the compressor hot leg without breaking the circuit. The 30A rating matches the single-phase condensing units in most walk-in box installations. |
| TRRS 3.5 mm breakout (e.g., SparkFun BOB-11570) | 1 | The SCT-013's output lead ends in a 3.5 mm three-conductor plug with the signal on tip and sleeve, and this breakout turns it into solderable pads. |
| 10 kΩ 1% resistor (CT bias pair) | 2 | Voltage-divider bias circuit that centers the CT's AC signal at Vcc/2 (1.65 V) so the ADC only sees positive voltages. |
| 10 µF electrolytic capacitor | 1 | Decouples the bias node so the CT signal can't modulate the divider mid-point. |
| Adafruit Magnetic Contact Switch / Door Sensor (product 375) | 1 | Normally-open reed switch in a pre-wired ABS enclosure. It screws to the door frame, and the included magnet mounts on the door itself. It closes below 13 mm separation and opens above it, and it holds up over millions of cycles with no moving parts to wear out. |
| 10 kΩ resistor, ¼W | 1 | External pull-up for the reed switch GPIO (supplements or replaces the internal STM32 pull-up for noise immunity). |
| AC/DC supply, 5V/2A (e.g., MeanWell IRM-10-5) | 1 | Supplies 5V/2A (10 W) DC from the cooler's dedicated circuit. The IRM-10-5 accepts 85–264VAC wide-range input, so it covers both 120VAC and 208/240VAC single-phase supplies. During a cellular session the MBGLW's Quectel EG916Q-GL modem averages ~250 mA (LTE Cat-1 bis). Where the modem falls back to GSM coverage, it can spike to ~2 A for a few milliseconds, and the supply's output bulk capacitance absorbs that burst within its 2 A continuous rating. Confirm the available supply voltage at the installation site before ordering. See Limitations for 24VAC/24VDC control-transformer installs. |
| External multiband LTE antenna, SMA male (e.g., SparkFun CEL-29622, 600 MHz–2.7 GHz hinged whip) | 1 | Mandatory for any metal-enclosure or metal mechanical-room installation. A Notecard antenna inside a steel NEMA 4X box can't radiate reliably. The 600 MHz–2.7 GHz pass-band covers every band the MBGLW's Quectel EG916Q-GL operates on (LTE Cat-1 bis B1/B2/B3/B4/B5/B7/B8/B12/B13/B18–B20/B25–B28/B66). Do not substitute a GPS/GNSS-only antenna (1.55–1.62 GHz), because it won't radiate on the cellular bands. Route the u.FL-to-SMA pigtail (next row) through a cable gland in the enclosure wall, then attach this external SMA antenna on the outside of the gland. |
| u.FL-to-SMA bulkhead pigtail cable, ~150 mm (e.g., Adafruit product 851) | 1 | Connects the Notecard's onboard u.FL antenna port to the external SMA panel-mount antenna. You need it whenever you use an external SMA antenna, since without this cable the antenna has no electrical path to the Notecard radio. A 150–200 mm length routes cleanly inside a ~6×4×2″ NEMA box without extra coiling. |
| NEMA 4X enclosure, ~6×4×2″ | 1 | Moisture- and hose-down-rated housing that suits the cold, wet environment of a walk-in mechanical room. |
All Blues hardware ships with an active SIM that includes 500 MB of data and 10 years of service, with no activation fees and no monthly commitment.
5. Wiring and Assembly
Safety: qualified personnel only. This assembly connects to mains-voltage AC power. A licensed electrician must install the AC/DC supply (wiring it to the cooler's dedicated circuit) in compliance with applicable electrical codes (NEC Articles 100 and 110 and local amendments). Apply lockout/tagout procedures to the circuit breaker before opening any conduit or junction box. Once installed, all three sensors (temperature probe, CT clamp, reed switch) are electrically non-invasive. The CT clamps over the wire without breaking the circuit and is isolated from the mains conductor, but the supply wiring itself is not. Don't energize the enclosure until all wiring is complete and the enclosure lid is secured.
All host I/O goes to the Notecarrier CX dual 16-pin header, and the Notecard Cell+WiFi seats in the carrier's M.2 slot. An external antenna is mandatory in a metal walk-in box or steel mechanical-room cabinet, because a Notecard antenna inside a metal enclosure can't radiate reliably. Route a short u.FL-to-SMA pigtail from the Notecard's u.FL port through a cable gland in the NEMA 4X enclosure wall and attach it to the panel-mount SMA whip antenna outside. For bench validation, Mojo sits inline between the 5V supply and the Notecarrier's +VBAT pad.
Pin-by-pin connections:
- 3V3 → DS18B20 red wire (VDD); top of the upper 10 kΩ CT bias resistor (the divider's high leg); top of the 10 kΩ door-switch pull-up resistor.
- GND → DS18B20 black wire (GND); bottom of the lower 10 kΩ CT bias resistor (the divider's low leg); 10 µF capacitor negative terminal; one terminal of the reed switch (either terminal, since it's just a switch).
- D5 → DS18B20 yellow wire (data); 4.7 kΩ pull-up from D5 to 3V3 wired here.
- A0 → CT TRRS breakout tip. Build the bias network as a series divider, with one 10 kΩ from +3V3 to the bias node and one 10 kΩ from the bias node to GND, so the bias node sits at Vref/2 ≈ 1.65 V. Connect the 10 µF capacitor from the bias node to GND (it decouples the bias rail to AC ground and is not in series with the signal). The breakout's sleeve lands on the bias node, so the CT sits between the bias node and A0 and neither CT lead goes to GND. A0 then sees 1.65 V DC plus the CT's AC swing (about ±1.41 V peak at 30 A), which stays within the 0–3.3 V ADC window. Don't wire the sleeve to GND. The CT's built-in burden (about 60 Ω) would pull the bias node down to roughly 20 mV, and the ADC would clip the negative half of every cycle.
- D6 → one terminal of the reed switch; 10 kΩ pull-up from D6 to 3V3. The
reed switch's other terminal goes to GND, so under
INPUT_PULLUPthe pin reads LOW when the door is closed and HIGH when it's open. - ATTN → D9 (host-sleep jumper). Fit a short jumper between the ATTN and
D9 pins on the Notecarrier CX header. Both are on the same 16-pin header
(
ATTNat P1 pin 3,D9at P1 pin 12). Other Notecarrier CX designs useD5for this jumper, but the DS18B20 takes it here, so the firmware setsCX_ATTN_PINtoD9. The Notecard pullsATTNlow while the host sleeps and raises it when the next sample is due, and the rising edge onD9wakes the host from STOP2. Leave the Notecarrier CX EN pin unconnected. On the CX it enables the shared 3.3 VVIOrail (host, Qwiic,3V3_OUT, and Notecard I/O), so driving it fromATTNwould brown out the whole board instead of putting the host to sleep. Putting a Host to Sleep Between Sensor Readings walks through the full pattern. - SDA / SCL → Notecard I²C (Notecarrier CX routes these to the M.2 slot internally; no external wiring needed).
- +VBAT → Mojo
LOADoutput (bench use only); MojoBATinput ← 5V DC output of the MeanWell IRM-10-5 (or equivalent 5V/2A AC/DC supply), which is wired to the AC service conductors in the NEMA 4X enclosure.
CT installation note. The SCT-013-030 is a split-core clamp. Open the clamp and pass it over one hot leg of the compressor's circuit. Don't clamp both legs, because their magnetic fields cancel and you'll read zero. Route the TRRS lead back to the NEMA 4X enclosure through a cable gland. The compressor circuit is line-voltage, so a qualified electrician must do the installation following applicable codes and lockout/tagout procedures. Once clamped, the CT itself is entirely non-contact and electrically isolated.
DS18B20 placement. For accurate air temperature, put the stainless probe tip
in open box air at mid-box height or along the return-air path, away from both
the evaporator fan discharge and the door. That spot gives the most
representative reading of box air temperature. It's a useful proxy for
stored-product conditions, but it isn't a direct product core-temperature
measurement. Stay away from the evaporator fan discharge. It's the coldest zone
in the box and reads several degrees below true representative air
temperature, so a probe there biases temperature-to-target deviation
calculations low and delays temp_high alerts. Skip the door zone too, since
it gets warm infiltration air on every open cycle. Keep the probe away from
direct coil contact for the same reason.
Reed switch installation. Mount the ABS switch body on the door frame and
the magnet on the door, aligned so the two parts are within 10 mm of each other
when the door is fully closed. Test continuity before you close the enclosure.
In the debug log (ST-LINK virtual COM port, with DEBUG_SERIAL enabled), pin D6
should read LOW with the door closed and HIGH with it open.
6. Notehub Setup
-
Create a project. Sign up at notehub.io and create a project. Copy the ProductUID and paste it into
firmware/cooler_monitor/cooler_monitor_helpers.has thePRODUCT_UIDconstant before flashing. -
Claim the Notecard. Power up the unit. The Notecard associates itself with your project on its first cellular connection.
-
Create Fleets. The natural grouping for a multi-site cooler program is one Fleet per banner or region, since the stores in a given franchise territory usually share the same setpoint targets and alert thresholds. Smart Fleet rules can break out exceptions automatically (e.g., flagship stores with tighter temperature tolerances) without reassigning devices by hand.
-
Set environment variables. Every variable below is optional, and the firmware defaults apply until you override them. Any variable you set in Notehub takes effect on the device's next inbound sync, with no reflash. To set a variable in Notehub: go to your project → Devices (or Fleet) → scroll down to Settings/Environment Variables → add or update the variable and save. The firmware sets the Notecard's
inboundsync interval to2 × summary_interval_min(120 minutes at the defaultsummary_interval_min = 60), and that's when the Notecard pulls fresh env-var values from Notehub into its on-device cache. The host picks up the new values on its next 60-second wake. So the worst-case time from "save in Notehub" to "value applied on the device" is roughly the inbound interval (up to 120 minutes by default) plus one sample cycle (~60 seconds). To exercise the full path quickly during commissioning, lowersummary_interval_min(the firmware keeps the inbound interval at twice that value) or trigger an immediate session withhub.syncfrom the Blues In-Browser Terminal.Variable Default Purpose sample_interval_sec60Seconds between sensor reads and state updates. summary_interval_min60Minutes between cooler_summary.qoNotes. Also re-applieshub.set outboundto keep cellular cadence in sync. Thekwh_windowfield reflects energy accumulated across the scheduled sample intervals within this window.temp_setpoint_f35.0Target box temperature (°F). It goes out in every summary Note as setpoint_f, so the corporate dashboard can compute drift =temp_f − setpoint_fwithout a separate lookup.temp_alert_f40.0Box temperature (°F) above which a temp_highalert fires.door_open_alert_sec300How many continuous seconds the door can stay open before a door_open_longalert fires (default 5 minutes).compressor_on_amps2.0Amps threshold above which the compressor counts as running for runtime and kWh accumulation. volts_nominal120.0Nominal line voltage (V) used in the apparent-power kWh estimate. -
Configure routes. Add one route for
cooler_alert.qo(real-time delivery to a store-ops or facilities channel) and a second forcooler_summary.qo(to a long-term analytics store or BI tool). Because the two Notefiles are separate at the source, they can go to entirely different destinations without any filter logic inside the route.
7. Firmware Design
The firmware is a main sketch plus helper files:
firmware/cooler_monitor/cooler_monitor.ino
(entry point and sample cycle),
firmware/cooler_monitor/cooler_monitor_helpers.cpp
(Notecard config, sensor reads, and Note emission), and
firmware/cooler_monitor/cooler_monitor_helpers.h
(shared constants, types, and declarations). Host sleep (the card.attn
request, STOP2 entry, and ATTN wake) lives in
firmware/cooler_monitor/cx_sleep.h.
Dependencies:
- Arduino core for STM32
(
stm32duino/Arduino_Core_STM32). Blues Wireless Notecard(note-arduinolibrary). Install it from the Arduino Library Manager or witharduino-cli lib install "Blues Wireless Notecard".STM32duino Low Powerand its dependencySTM32duino RTC, both from the Library Manager. This library provides the STOP2 entry and ATTN-pin wake used incx_sleep.h.OneWirelibrary, from the Arduino Library Manager.DallasTemperaturelibrary, also from the Arduino Library Manager.
Build settings: The Notecarrier CX's host is the same STM32L433 as the Blues
Cygnet, so select Blues boards → Cygnet (pnum=CYGNET) and set USB
support (if available) to None (usb=none). If the USB CDC stack is
enabled and no USB host is attached (the Notecarrier CX USB-C switch in NC, or
no cable at all), the STM32's USB wakeup interrupt fires continuously and the
host never stays in STOP2. Debug output (when DEBUG_SERIAL is defined in
cooler_monitor_helpers.h) goes to the LPUART on the Notecarrier CX debug jack
instead, which an ST-LINK V3 exposes as a virtual COM port (see Serial Logging
With STLINK).
Modules
| Responsibility | Where |
|---|---|
Notecard hub.set configuration (power-up in setup() + every wake in loop() until confirmed) | hubConfigure() |
| Template registration for both Notefiles | defineTemplates() |
| Env-variable fetch and clamp on every wake | fetchEnvOverrides() |
Re-apply hub.set if summary_interval_min changed | applyHubSetIfChanged() |
| DS18B20 temperature read (timeout-polled, NaN sentinel) | readBoxTempF() |
| CT two-pass RMS current read | readCompressorAmps() |
| Reed-switch door-state read | readDoorOpen() |
| Accumulation, alert evaluation, summary trigger | runSampleCycle() |
| Immediate-sync alert emission | sendAlert() |
| Queued summary emission | sendSummary() |
Host sleep (STOP2 until the Notecard raises ATTN on D9) | cx_sleep.h: cxSleepBegin(), cxSleepUntilAttn() in loop() |
| Application state (held in RAM across sleep cycles) | AppState |
Sensor reading strategy
DS18B20. The firmware configures the probe in non-blocking mode
(setWaitForConversion(false)). After requestTemperatures(), it polls
isConversionComplete() in an 850 millisecond timeout loop instead of calling
delay(750). The host stays awake for the whole conversion. It's a
timeout-polled read within one wake cycle, not a conversion pipelined across
sleeps, but polling keeps the firmware from hanging forever if the sensor is
slow to respond. If the probe is disconnected or returns a value below –55 °C
or at 85 °C or above (including the 85.0 °C power-up sentinel the DS18B20 can
emit on a bus fault), readBoxTempF() returns NAN. The summary then emits
–9999 as a sentinel instead of a misleading zero, so downstream analytics can
tell "sensor failed" apart from a real measurement.
Current transformer. The SCT-013-030 outputs an AC signal centered at 0 V. A two-resistor 10 kΩ voltage divider from 3V3 to GND, with a 10 µF decoupling capacitor, puts a stable 1.65 V DC offset (Vcc/2) on the ADC pin. The firmware uses a two-pass algorithm. Pass one samples the ADC continuously for 150 milliseconds to compute the DC mean, and pass two samples for another 150 milliseconds and computes the RMS of the AC component around that mean. At 60 Hz one mains cycle is 16.7 milliseconds, so a 150 millisecond window covers about nine complete cycles. That's enough of the actual mains waveform for a well-conditioned RMS result. The firmware floors readings below 0.15 A to zero to suppress ADC noise when the compressor is off.
Reed switch. The firmware reads it with a simple digitalRead and
INPUT_PULLUP. The switch is normally open, and it closes (pulling D6 LOW) only
when the door is shut and the magnet is within 13 mm of the sensor. Door-open
events are edge-detected, so a transition from prevDoorOpen = 0 to
doorOpen = 1 increments the event count.
Event payload design
The firmware writes to two template-backed Notefiles. Templates let the Notecard store Notes as fixed-length binary records instead of free-form JSON, which shrinks on-wire payload size by roughly 3–5×. At 24 summary Notes per box per day across a thousand-store fleet, that difference grows quickly. In Notehub's Events tab, the events still show up as human-readable JSON, because the binary template format is internal storage only.
Template type encoding (used by the firmware in defineTemplates()):
14.1 = IEEE 754 4-byte float; 14 = 4-byte signed integer; 12 = 2-byte
signed integer. You specify these when you
register a template with the Notecard, so it knows how to pack and unpack each
field.
cooler_summary.qo (per summary window, queued):
{
"file": "cooler_summary.qo",
"body": {
"temp_f": 35.4,
"setpoint_f": 35.0,
"compressor_amps": 3.5,
"compressor_run_min": 32.0,
"door_opens": 5,
"door_open_sec": 300,
"kwh_window": 0.42,
"window_sec": 3600
}
}Here's what the fields mean. temp_f and compressor_amps are window
averages (sum of valid readings ÷ valid-reading count), while
compressor_run_min, door_open_sec, door_opens, and kwh_window are
window totals. window_sec is the sum of the scheduled sample intervals
that elapsed during the window. It counts sleep time only and leaves out the
awake time spent sampling. If sample_interval_sec doesn't divide evenly into
summary_interval_min × 60, the window overshoots by at most one sample
period. Downstream tools should use window_sec as the denominator for any
energy-rate or duty-cycle calculation (e.g. average watts =
kwh_window / window_sec × 3 600 000), keeping in mind that actual wall-clock
time runs marginally longer than window_sec because of the excluded awake
time. setpoint_f carries the current Notehub-configured corporate target (not
a value read from the cooler controller), so a dashboard can compute deviation =
temp_f − setpoint_f per record without a separate lookup. A –9999 in
temp_f signals a sensor fault (e.g. probe disconnected).
cooler_alert.qo (immediate, sync:true bypasses outbound cadence):
{
"file": "cooler_alert.qo",
"body": {
"alert": "door_open_long",
"temp_f": 38.1,
"amps": 10.4,
"door_open_sec": 300
},
"sync": true
}Low-power strategy
The sampling cadence (every 60 seconds by default) and the transmission cadence
(every 60 minutes) are independent of each other. setup() runs once at
power-up to configure the Notecard and the sensors, and each pass through
loop() is one sample cycle. After the cycle, cxSleepUntilAttn() (in
cx_sleep.h) sends a
card.attn sleep
request for sample_interval_sec seconds, waits for the Notecard to pull ATTN
low, and puts the STM32 host into STOP2, where it draws a few microamps and
keeps its RAM intact. When the interval is up, the Notecard raises ATTN. The
rising edge on D9 wakes the host, execution resumes on the next line of
loop(), and because the AppState struct lives in RAM, nothing needs to be
serialized to the Notecard. If the sleep request fails or ATTN never goes low
(jumper missing), loop() waits out the interval awake and tries again on the
next cycle.
Between cellular sessions, the Notecard idles in its own low-power mode. The walk-in cooler runs on 120VAC, so the absolute mAh budget isn't the constraint here. The sleep pattern still matters for enclosure thermal management and for porting the design to future battery-assisted variants.
Retry and error handling
hub.setis retried on every wake until confirmed.hubConfigure()returnsbool. The first call runs insetup()at power-up and stores its result instate.hubSetConfirmed. If that call fails, every later pass throughloop()hits anif (!state.hubSetConfirmed)branch that retrieshubConfigure()unconditionally, whether or notenv.getsucceeds. OncehubSetConfirmedis set, the device switches to the lighterapplyHubSetIfChanged()path, which re-issueshub.setonly whensummary_interval_minchanges. That way the device can't stay permanently unassociated while it quietly piles up Notes in its local queue.env.getchecks theerrfield on the response before it touchesbody, so a failed request leaves the current values in place instead of corrupting the config.- The two alert types carry independent cooldown timers (
doorAlertCooldownSec,tempAlertCooldownSec) measured in wall-clock seconds, so timing stays accurate whensample_interval_secchanges. Each alert can fire at most once per 30-minute window, which keeps a door left open for a while or a sluggish compressor from firing dozens of identical alerts on consecutive wakes. - If a Notehub env var changes
summary_interval_min,applyHubSetIfChanged()re-issueshub.setwith the updated outbound cadence, so the Notecard's cellular session timing follows the new summary period instead of drifting. This path only runs afterhubSetConfirmedis true.
Key code snippet 1: template definition
Template type hints tell the Notecard how to pack each field (see "Template type
encoding" above). door_open_sec and window_sec use 14 (4-byte signed
integer) instead of 12 (2-byte signed integer, max ~9 hours), because long
summary intervals or a stuck door can pile up values that overflow a 2-byte
integer. door_opens, on the other hand, uses 12 (2-byte signed, up to ~32k
events), since a realistic window rarely tops 50 door cycles. The Blues
template data-type
guide
has the complete field-type reference. FILE_SUMMARY is cooler_summary.qo,
defined in cooler_monitor_helpers.h.
J *req = notecard.newRequest("note.template");
// ...
JAddStringToObject(req, "file", FILE_SUMMARY);
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "temp_f", 14.1);
JAddNumberToObject(body, "setpoint_f", 14.1);
JAddNumberToObject(body, "compressor_amps", 14.1);
JAddNumberToObject(body, "compressor_run_min", 14.1);
JAddNumberToObject(body, "door_opens", 12);
// ...
JAddNumberToObject(body, "door_open_sec", 14);
JAddNumberToObject(body, "kwh_window", 14.1);
// ...
JAddNumberToObject(body, "window_sec", 14);
J *rsp = notecard.requestAndResponse(req);
if (!rsp || notecard.responseError(rsp)) {
DBG_PRINTLN("[tmpl] summary template registration failed");
ok = false;
}
if (rsp) notecard.deleteResponse(rsp);Key code snippet 2: immediate-sync alert
sync:true skips the outbound cadence, so the Notecard wakes the radio
immediately. runSampleCycle() calls sendAlert() with door_open_long or
temp_high, and arms that alert's 30-minute cooldown only when it returns true.
bool sendAlert(const char *alert, float tempF, float amps, uint32_t doorSec) {
J *req = notecard.newRequest("note.add");
// ...
JAddStringToObject(req, "file", FILE_ALERT);
JAddBoolToObject(req, "sync", true);
J *body = JAddObjectToObject(req, "body");
JAddStringToObject(body, "alert", alert);
JAddNumberToObject(body, "temp_f", (double)tempF);
JAddNumberToObject(body, "amps", (double)amps);
JAddNumberToObject(body, "door_open_sec", (int32_t)doorSec);
J *rsp = notecard.requestAndResponse(req);
if (!rsp) {
DBG_PRINT("[alert] no response for: "); DBG_PRINTLN(alert);
return false;
}
bool ok = !notecard.responseError(rsp);
if (!ok) { DBG_PRINT("[alert] note.add error for: "); DBG_PRINTLN(alert); }
notecard.deleteResponse(rsp);
return ok;
}Key code snippet 3: sleeping until the Notecard raises ATTN
cxSleepUntilAttn() asks the Notecard to hold ATTN low for cfgSampleSec
seconds, confirms the pin has gone low, and puts the STM32 into STOP2. Since the
host stays alive, the firmware checks the result of the card.attn call instead
of treating it as fire-and-forget. The DS18B20 occupies D5, so this sketch
overrides the wake pin before including the header.
#define CX_ATTN_PIN D9 // D5 is the DS18B20 data line; wake on D9 instead
#include "cx_sleep.h"
void setup() {
// ...
// Arm the ATTN wake: the Notecard raises ATTN (D9) to end each sleep.
cxSleepBegin();
}
void loop() {
// ...
runSampleCycle(state);
// Sleep until the Notecard raises ATTN cfgSampleSec from now.
// ...
if (!cxSleepUntilAttn(notecard, cfgSampleSec, NULL, log)) {
// The Notecard didn't take the sleep request, or ATTN never went low
// (check the ATTN -> D9 jumper). Keep the sample cadence and try again.
DBG_PRINTLN("[sleep] ATTN sleep failed — waiting out the interval awake");
delay(cfgSampleSec * 1000UL);
}
}8. Data Flow
Collected every sample_interval_sec (60 seconds by default): box air
temperature (°F), compressor RMS amps, and door open/closed state.
Accumulated within each summary window: compressor apparent kWh (V × I × t / 1000, compressor hot leg only), compressor runtime minutes, total door-open seconds, and door-open event count (rising-edge transitions).
Transmitted:
cooler_summary.qo: one record persummary_interval_min(about once an hour by default), queued locally and flushed during the Notecard's scheduled outbound cellular session.temp_fandcompressor_ampsare window averages (mean over all valid reads during the window).compressor_run_min,door_open_sec,door_opens, andkwh_windoware window totals.window_secis the sum of scheduled sample intervals for the window, so use it as the denominator for energy-rate calculations. It counts sleep time only, and actual wall-clock elapsed time is marginally longer because awake time is excluded.setpoint_fcarries the current Notehub-configured corporate target, so downstream tools can compute deviation =temp_f − setpoint_fper record without a separate lookup.–9999signals a sensor fault fortemp_f, whereNANwould be meaningless in JSON.cooler_alert.qo: sent only when a threshold trips, withsync:trueto skip the outbound timer. The alert cooldown allows no more than one alert per type per 30-minute window.
Routed. Both Notefiles arrive at Notehub and go from there to whatever downstream routes the project configures. The two filenames are different on purpose, so each can go to its own destination with its own urgency and no route-level filtering.
Alert triggers:
door_open_long: the door has been open continuously for at leastdoor_open_alert_secseconds (default 5 minutes). While the door stays open, it fires again at most once per 30-minute cooldown window. One page-out every half hour is enough for store ops to investigate.temp_high: box temperature exceedstemp_alert_f(default 40 °F). The alert carries the current amps reading, so the responder can see right away whether the compressor is running (equipment issue) or off (power outage, breaker trip).
9. Validation and Testing
Expected steady-state cadence. A healthy cooler sends one
cooler_summary.qo event about every summary_interval_min (60 minutes by
default) and no cooler_alert.qo events unless a threshold is breached. As a
rough reference, a mid-size commercial walk-in cooler (800–1200 cu ft) at a
typical QSR might show:
- Compressor runtime: 35–55% duty cycle (21–33 minutes per 60-minute window)
- Compressor apparent kWh: 0.3–0.6 kWh per window
- Door opens: a handful per window (depends on traffic). The firmware only counts an opening that's still open at a sample, so quick in-and-out trips between samples don't show up
- Door open time: 60–300 seconds cumulative per window, in whole sample intervals (60 seconds each by default)
- Temperature drift: 0–2 °F above setpoint (after stabilization)
These are order-of-magnitude benchmarks, and real figures vary a lot with box
volume, insulation, ambient conditions, product load, and defrost cycle
frequency. If temp_f runs consistently more than 3–4 °F above
temp_setpoint_f, look into it no matter what the runtime figures say.
Simulating alerts during commissioning. There are two quick threshold
tests. First, set temp_alert_f just below the current ambient in Notehub's
environment-variable editor, wait one inbound sync (~120 minutes, or send
hub.sync from the blues.dev In-Browser Terminal), and watch for a temp_high
alert in the Events tab. Second, open the cooler door and leave it open past
door_open_alert_sec to trigger door_open_long. Restore the original
threshold after each test.
Using Mojo to validate power behavior. During bench testing, the Mojo sits inline between the 5V supply output and the Notecarrier CX +VBAT pin and reports cumulative mAh over its Qwiic I²C link. Mojo measures at the +VBAT rail, so it sees the whole assembly: the Notecard, Notecarrier CX, STM32 host, DS18B20, CT bias circuit, and reed switch, not just the Notecard.
(a) Published Notecard Cell+WiFi (MBGLW) current figures. The table below cites figures from the Blues low-power design documentation and the MBGLW datasheet. They're Notecard-only figures, and the Mojo reading will be higher because it includes the Notecarrier and sensors.
| Operating mode | Published current (Notecard only) | Source |
|---|---|---|
| Idle, radio off (between syncs) | ~8–18 µA @ 5V | Blues low-power docs |
| LTE Cat-1 bis session active (primary RAT) | ~250 mA average. The supply rail must handle brief current peaks, so see Blues cellular power-supply guidance for sizing | MBGLW datasheet |
| GSM fallback (rare; areas without LTE coverage) | Up to ~2 A for a few milliseconds during a GSM transmit burst, absorbed by the supply's output bulk capacitance within the IRM-10-5's 2 A continuous rating | MBGLW datasheet |
| Per-session energy (single queued Note, bench test) | ~0.3 mAh | Blues low-power design tests |
| WiFi active (opportunistic fallback) | Not specified separately in the MBGLW datasheet. The integrated Silicon Labs WFM200S WiFi module adds to the cellular idle baseline when active, so check the WFM200S transceiver datasheet for characterization data | MBGLW datasheet, hardware overview |
(b) Expected Mojo trace (whole assembly at +VBAT). Your Mojo readings will sit above the Notecard-only floor because they include the Notecarrier CX regulator, the CT bias voltage divider (~165 µA continuous from the 3V3 rail), and the connected sensors' quiescent draw. Here's the expected pattern, with approximate ranges for each firmware phase:
| Phase | Approx. whole-assembly current at +VBAT |
|---|---|
| Host in STOP2 between samples (card.attn sleep, Notecard idle) | ~0.5–1 mA, well above the Notecard-only µA floor. The host in STOP2 adds only ~1–2 µA, so the CT bias divider and Notecarrier CX regulator quiescent current dominate |
| Host active during sampling (~1–2 seconds per wake) | ~15–30 mA spike from the active STM32L433 host, the 750 millisecond DS18B20 conversion, and the 2 × 150 millisecond CT ADC passes |
| Cellular outbound session, LTE Cat-1 bis (~30–60 seconds per hour) | ~250–300 mA: the Notecard modem (~250 mA average) plus Notecarrier and sensor overhead. The 5V/2A supply handles this with plenty of headroom. In rare GSM-only coverage areas the modem bursts to ~2 A for a few ms, and the supply's output capacitance absorbs that within its 2 A rating. |
The shape of the trace tells you as much as the absolute level. You want to
see a stable sub-milliamp floor between wakes, a ~15–30 mA spike every 60
seconds lasting 1–2 seconds (one sample cycle), and a ~250–300 mA plateau once
per summary window lasting tens of seconds (the LTE Cat-1 bis cellular session).
A persistent multi-mA floor between wakes, above the ~0.5–1 mA sleep baseline,
means the host isn't entering STOP2. Check the ATTN→D9 jumper and confirm
the firmware was built with the usb=none board option. If cellular plateaus
come more often than the configured summary cadence, an alert is firing and
triggering sync:true sessions outside the normal schedule. Both of those
draws are a negligible fraction of the 5V/2A supply's capacity. Where the bench
measurement really pays off is in catching firmware regressions that
accidentally leave the host awake all the time.
10. Troubleshooting
Device claims to Notehub but no events appear after 2+ hours.
- Make sure the Notecard firmware is up to date. In Notehub, go to Devices → click your device → Device Info → Notecard version. If it's out of date, trigger a Notecard firmware update.
- Confirm the ProductUID in the firmware matches your Notehub project. The Notecard won't transmit to the wrong project.
- In the Blues In-Browser Terminal (top-right button), run
hub.statusto check the Notehub connection andcard.wirelessto check cellular signal (rssiin dBm andbarsin itsnetobject). If you see"status": "disconnected", wait 2–3 minutes and try again, since the first connection can take several cycles. - Check that the templates are registered. The Events tab only lists Notes that
have arrived, so it can't show you an empty Notefile. Instead, send
{"req":"note.template","file":"cooler_summary.qo","verify":true}from the In-Browser Terminal, which returns the templatebodyif one is set. If it isn't, thedefineTemplates()function failed, so check the firmware logs on the ST-LINK virtual COM port (Notecarrier CX debug jack) withDEBUG_SERIALenabled.
Events appear but temp_f or compressor_amps show NaN or -9999.
temp_f = -9999: the DS18B20 probe is disconnected or shorted, or the 4.7 kΩ pull-up is missing or open. Check D5 connectivity and the probe's 1-Wire bus.compressor_amps = 0.0: the CT is reading zero. Check that the CT clamp is around only one hot leg of the compressor circuit, because clamping both cancels the field. Make sure the TRRS connector is fully seated at the SCT-013 input, and check that the A0 bias-circuit voltage is ~1.65 V (measure with a multimeter between A0 and GND).
Door-open alerts keep firing (once per 30-minute cooldown) even when the door is closed.
- D6 reads HIGH with the door shut, so the firmware thinks it's open. The magnet
is out of range of the switch, the reed switch has failed open, or the D6 or
GND lead is broken. Confirm the switch reads LOW on D6 when the door is
physically closed and HIGH when it's open. You can check with a pin voltage
meter, or upload a sketch that prints
digitalRead(D6)to the ST-LINK virtual COM port. - Realign the magnet to within 10 mm of the switch. If D6 still reads HIGH with the magnet right against it, replace the switch.
Cellular sessions happen more frequently than expected (not just every
summary_interval_min).
- Alerts are firing and triggering
sync:truesessions outside the normal cadence. Look forcooler_alert.qoin the Events tab. If it's there, one of the two alert rules is active, so check that thetemp_alert_fanddoor_open_alert_secthresholds aren't too tight for your operating environment.
Device wakes and samples but the Mojo trace never drops to the sleep floor between cycles.
- The host isn't entering STOP2. Check the
ATTN→D9jumper on the Notecarrier CX header. Without it,cxSleepUntilAttn()sees thatATTNnever goes low, waits out the interval awake, and retries. Also confirm the firmware was built with USB support set to None (usb=none), and verify I²C connectivity between the Notecarrier and Notecard. Make sure the firmware has no blocking delays ordelay()calls outside of timeout-polled sensor reads (e.g., inloop()beforecxSleepUntilAttn()).
11. Limitations and Next Steps
A reference design that has to drop onto 800 different walk-in boxes has to be modest, on purpose, about what it measures and what it concludes. The rest of this section spells out where this one draws those lines.
Simplified for this POC
Each simplification below was a deliberate scope choice. Once basic fleet visibility is paying for itself, these are the places a particular operator will want to add a sensor, calibrate a probe, or wire in a controller-side integration.
Sensor reads are sample-based, not interrupt-driven. The firmware polls all
three sensors once per sample_interval_sec (60 seconds by default). Any door
opening, door closing, or compressor start/stop that happens and finishes
within one 60-second sleep interval goes undetected. That quantizes edge timing
to the sample period, which directly affects door_opens, door_open_sec,
compressor runtime, and kwh_window. Any of them can undercount if events are
shorter than the sample period. door_open_long alert timing is quantized the
same way. A door that opens just after one sample and closes just before the
next may not trip the alert, even if it was physically open longer than
door_open_alert_sec. Lowering sample_interval_sec with an environment
variable improves resolution, but it increases host awake time and may increase
alert frequency (more frequent checks catch alert conditions sooner and may
trigger sync:true sessions more often). Scheduled outbound cellular sessions
are still governed by summary_interval_min and don't increase with the sample
interval alone.
AC supply voltage. The MeanWell IRM-10-5 in the BOM accepts 85–264VAC wide-range input and handles both 120VAC and 208/240VAC single-phase, which covers most walk-in condensing-unit installations. Some mechanical rooms only provide a 24VAC or 24VDC control-transformer rail instead of mains voltage. Those installs need a different converter (a 24VAC-input or 24VDC-input step-down regulator) in place of the AC/DC supply listed here. Always confirm the available supply with the field electrician before ordering components.
Compressor apparent energy only, not total cooler energy. The single CT
clamps one hot leg of the compressor circuit, so evaporator fans, defrost
heaters, door heaters, controls, lighting, and other loads on the cooler's
electrical service aren't measured. kwh_window is a compressor-energy proxy,
not total cooler or site energy consumption. If total box energy is the
metric you want, you'd need a revenue-grade multifunction meter or more CT
channels for the non-compressor loads. The estimate also uses apparent power
(V × I) instead of real power (V × I × PF). A typical single-phase hermetic
compressor motor has a power factor of 0.75–0.90, so the estimate will
overstate actual compressor energy by 11–33 %. For billing-grade energy
monitoring, use a calibrated kilowatt-hour meter with a pulse output. The
single-CT apparent-power approach gives you a useful relative metric for
comparing compressor behavior across units and tracking trends over time, not
an absolute kWh figure.
Single-phase only. Larger walk-in boxes (over 4,000 cu ft) or walk-in freezers may run three-phase compressors, which need three CTs, more ADC channels, and firmware changes to sum the per-phase apparent power.
Temperature sensor without calibration offset. The DS18B20 probe is factory-calibrated to ±0.5 °C (±0.9 °F). For temperature-to-target deviation monitoring at ±1 °F precision, an in-situ calibration offset stored as an environment variable would remove the unit-to-unit spread.
Temperature-to-target deviation is a downstream-derived metric, not an
on-device rule, and the cooler controller's thermostat setpoint is not
observed. The device has no connection to the cooler's onboard controller or
thermostat. setpoint_f in every summary Note is the corporate target
temperature configured as a Notehub environment variable (temp_setpoint_f),
not a value read from the cooler's electronic or mechanical controller.
Monitoring the actual controller setpoint would take an integration with the
cooler controller (over Modbus or a proprietary serial interface). A downstream
dashboard can compute deviation (temp_f − setpoint_f) per record, but the
firmware itself never compares temperature against setpoint_f. Its only
on-device temperature alerting is the temp_high rule, which fires when
temp_f exceeds the separate temp_alert_f threshold (default 40 °F).
temp_high is a fixed threshold, not a setpoint-relative check, so a
legitimate spike during a freight delivery (door held open 10 minutes) triggers
the same alert as a genuine refrigerant problem. Gating the alert on
compressor runtime and recent door activity would cut those false positives
considerably.
No GNSS. Notecard Cell+WiFi includes a GNSS module, but this project doesn't
use it, because a walk-in cooler box stays put. If mapping dashboards need the
site's lat/lon, you can bootstrap a one-time site location from cellular tower
triangulation with
card.location.
Production Next Steps
With basic fleet visibility in place, these extensions are the usual next moves. They start with what's most immediately useful and end with the items that lean hardest on infrastructure.
A per-unit calibration offset for the DS18B20 (temp_offset_f env var,
applied in readBoxTempF() at runtime) is the first refinement, and it
tightens per-probe accuracy across the fleet.
A door_open_max_sec field on the summary payload would capture the longest
single uninterrupted door-open event in the window. That helps tell a normal
rapid-turnaround pattern apart from one sustained-open incident, which
door_open_sec (total) can't show on its own.
Three-phase compressor support brings larger boxes into scope with three SCT-013-030 CTs on A0/A1/A2, RMS per leg, and summed apparent power.
kWh power-factor correction through a power_factor environment variable
(0.80 default) lets ops dial in a correction for each unit type after a
baseline period against a reference meter, which narrows the
apparent-vs-real-power gap described above.
Notecard Outboard DFU adds over-the-air firmware updates, so threshold-algorithm improvements can roll out fleet-wide without truck rolls.
A cooler_config.qi inbound Notefile would let the ops team request a
diagnostic dump, a full state snapshot on demand, without waiting for the next
summary.
12. Summary
The corporate energy team that used to stall at the 50-store pilot now has a fleet-wide view of compressor apparent kWh per window, door-open seconds, and temperature drift against a setpoint they configure from a browser. It takes one cellular SKU and three sensors, and the field tech never has to ask anyone for a WiFi password. The outlier box running double the compressor-hours of its peers now shows up on a dashboard instead of in next month's utility bill.