Rooftop HVAC Predictive Maintenance

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/rtu_predictive_maintenance/ so it can learn how the device works and help you adapt it to your hardware.
This project is a retrofit downtime prevention pack for commercial rooftop HVAC units. A handful of inexpensive sensors and a Blues Notecard Cell+WiFi turn an ordinary rooftop unit into one you can monitor remotely and maintain predictively. The pack catches refrigerant loss, short-cycling, and clogged filters before the unit fails and a tenant loses cooling.
What you'll have when you're done: a weatherproof, line-powered sidecar bolted to a commercial rooftop air-conditioning unit. Every minute it samples four sensors and runs three failure-mode checks locally. If a threshold trips, it pages your on-call queue within ~60 seconds over cellular, without involving site IT or modifying the unit itself. Hourly summaries go out on a separate event stream for trending and analytics, and operators can retune thresholds in the field from the cloud without changing any firmware.
1. Project Overview
The problem. In HVAC, an RTU (rooftop unit) is a self-contained packaged HVAC system mounted on the roof of a commercial building, with the compressor, condenser, evaporator, blower, and controls all in one box. RTUs are the workhorse of light commercial cooling. Nearly all grocery stores, restaurants, strip-mall tenants, and small warehouses are conditioned by one or more RTUs sitting above the suspended ceiling.
A commercial RTU sits on a roof where it can't see building WiFi, and when it fails, a grocery store or restaurant loses cooling and goes dark. The HVAC failures behind most unplanned downtime are gradual. A refrigerant leak slowly eats away at cooling capacity over weeks. When a contactor wears out, it starts chattering, and the compressor short-cycles several times an hour instead of running in long, steady pulls. Then there's the neglected filter that chokes airflow across the coil. None of these are invisible, but they're all easy to miss from inside the building, especially when nobody's looking. You can detect each one hours to days before the actual outage, if someone (or something) happens to be watching.
This project is that watcher. It's a retrofit sidecar that straps to the RTU chassis on the roof, samples four sensors every minute, and pages the service technician before the walk-in cooler hits 50°F on a Saturday night. In Blues terms, it's a downtime-prevention device-to-cloud system. It monitors continuously at the edge, detects failures with rules in firmware, and sends proactive-service alerts through Blues Notehub to whatever on-call system the fleet owner already uses. Detection comes down to three scalar threshold checks, which catch the same impending-failure patterns HVAC technicians look for on a service call.
Why Notecard. RTUs have no line of sight to indoor WiFi access points, and HVAC OEMs and service companies need one SKU that works the same way in a strip mall and in a warehouse. Cellular takes per-site IT out of the picture completely. You don't fill out a network form, pair with an AP, or chase an IT ticket. Notecard Cell+WiFi keeps WiFi as an optional fallback for the occasional site that happens to have a rooftop-accessible AP, and the deployment 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.
2. System Architecture
Device-side responsibilities. Inside the sidecar, the Notecarrier CX's STM32
host wakes once a minute, sweeps the four sensors, and runs the three
failure-mode checks before the rooftop unit has time to change much. Then it
hands the resulting Note (a JSON payload) to the Notecard over I²C and uses
card.attn to sleep in
STM32 STOP2 (a few µA) until the Notecard's ATTN pin wakes it at the next
interval. The host never deals with JSON marshaling, AT commands, or serial
buffers, and it's asleep more than 99% of the time it sits on the roof.
Notecard responsibilities. Everything the host queues up lands in the
Notecard's on-device Notes store. Summary
records go out on the configured
hub.set outbound
cadence (60 minutes by default). Any alert tagged sync:true skips the queue,
and the cellular radio comes up right away to send it. The Notecard also handles
one-time GNSS site geolocation on its own and pulls environment
variables
(cloud variables) on every inbound sync. That's how a fleet operator can retune
thresholds across a thousand rooftops without anyone touching firmware.
Notehub responsibilities. The Notecard's embedded global SIM gets it onto
whichever carrier covers the rooftop. From there, events flow into
Notehub, which ingests and stores every one and applies
the project's routes, which can optionally send the data on to your cloud
platform of choice. Alerts and summaries land in separate
Notefiles on purpose. You can route
rtu_alert.qo JSON-based Notes straight to an on-call paging service or CMMS
(computerized maintenance management system) while rtu_summary.qo flows to a
long-term analytics store, and the routes don't need any filtering logic.
Routing to the cloud (high level only). Notehub can route to HTTP, MQTT, AWS, Azure, GCP, Snowflake, Twilio, and several other destinations, and route setup is specific to each project. This project doesn't ship with a particular downstream endpoint, but pointing your data at the cloud endpoint you pick is easy. The Notehub routing docs show how.
3. Technical Summary
If you want the fastest path from "parts on the bench" to "first event in Notehub":
- Notehub. Create a free Notehub project and copy its ProductUID.
- Wire the bench rig. You need a Notecarrier
CX +
Notecard MBGLW + two thermistor
dividers on A0/A1 + CT bias network on A2 + SDP810 on I²C + a jumper from
ATTNtoD5. The full pinout is in §5. - Edit one line of
firmware/rtu_predictive_maintenance/rtu_predictive_maintenance.ino. SetPRODUCT_UIDto your project's value (line 32). - Flash. Run
arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=noneand thenarduino-cli upload. Full instructions are in §7.1. - Watch. Open Notehub → your project → Events tab. You should see a
_session.qowithin minutes, anrtu_summary.qowithin an hour or two, and any threshold trips asrtu_alert.qoin real time.
The rest of this page walks through each step in more detail and explains why the firmware works the way it does.
For a real rooftop install rather than a bench bring-up, read §11 Limitations too before you commit to a power topology.
Here's a sample Note from this device:
{
"supply_f": 52.4,
"return_f": 71.8,
"delta_t_f": 19.4,
"compressor_amps": 14.7,
"filter_dp_pa": 31.2,
"starts": 3,
"runtime_min": 41.0
}4. Hardware Requirements
| Part | Qty | Rationale |
|---|---|---|
| Notecarrier CX | 1 | Integrated carrier board with an onboard STM32L433 host, so this sensor mix 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 takes per-site IT out of the picture, and the WiFi fallback is there for sites that happen to have it. |
| Blues Mojo (optional, bench-only) | 0–1 | Coulomb counter on the power rail that gives you ground-truth energy numbers during bench bring-up. See §9 and §11. |
| 10 kΩ NTC thermistor, β=3950, waterproof duct probe | 2 | Duct-mount supply and return air temperature for cooling delta-T. |
| 10 kΩ 1% resistor (divider series leg) | 2 | Pull-up for each thermistor divider. |
| SCT-013-030 split-core CT, 30A / 1V AC (voltage-output CT with a built-in burden resistor, so don't add one) | 1 | Non-invasive compressor current sensing. The 30A range matches single-phase light-commercial RTU compressors. SparkFun's SEN-11005 is a different 30A CT with a 15 mA current output, so if you use it, add a 68 Ω burden resistor across its leads to get the same 1V RMS at 30A. |
| TRRS 3.5 mm breakout (e.g. SparkFun BOB-11570) | 1 | The SCT-013's output lead ends in a TRRS plug. |
| 10 kΩ 1% resistor (bias pair) | 2 | CT bias circuit that centers the AC signal at Vref/2 so the ADC only sees positive voltages. |
| 10 µF electrolytic capacitor | 1 | Bias-circuit decoupling. |
| Sensirion SDP810-125Pa differential-pressure sensor | 1 | I²C DP sensor across the filter. A continuous reading shows the trend toward clogging, while a binary DP switch only tells you once you're already there. |
| ¼″ silicone tubing, ~2 ft | 1 | Taps the SDP810 across the filter frame. |
| AC/DC supply, 5V/2A output (e.g. MeanWell IRM-10-5) | 1 | Derives 5V DC from 120VAC line power at the RTU service disconnect, so the unit runs on permanent grid-tied power. See Limitations for the 24VAC-input alternative. |
| NEMA 4X enclosure, ~6×4×2″ | 1 | Rooftop-rated housing. |
All Blues hardware ships with an active embedded SIM that includes 500 MB of data and 10 years of service, with no activation fees and no monthly commitment.
5. Wiring and Assembly
All host I/O lands on the Notecarrier CX dual 16-pin header. The Notecard Cell+WiFi (MBGLW) seats in the carrier's M.2 slot, and the cellular, GNSS, and WiFi antennas run as u.FL leads to external antennas mounted on the roof. The Mojo sits inline between the 5V supply and the Notecarrier's +VBAT pad and reports cumulative mAh to the Notecard over its Qwiic connector.
Pin-by-pin:
- +3V3 → top of each thermistor's 10 kΩ series resistor, SDP810
VIO, top leg of the CT bias divider (10 kΩ from +3V3 to bias node). - GND → bottom of each thermistor NTC, SDP810
GND, bottom leg of the CT bias divider (10 kΩ from bias node to GND), bottom of the 10 µF bias-decoupling capacitor. - A0 → wiper (mid-point) of the supply-air thermistor divider.
- A1 → wiper of the return-air thermistor divider.
- A2 → CT signal node. The two 10 kΩ resistors form a divider from +3V3 to GND, and the midpoint (the bias node) sits at Vref/2 ≈ 1.65 V. The 10 µF capacitor goes from the bias node to GND. It decouples the bias rail to AC ground and is not in series with the signal. The CT's TRRS plug bridges A2 and the bias node, with tip → A2 and sleeve → bias node (or the other way around, since the AC signal is symmetric). A2 then sees 1.65 V DC with the CT's AC swing on top, which keeps the signal inside the 0–3.3 V ADC window.
- SDA / SCL → SDP810
SDA/SCL(the Notecarrier has pull-ups on-board). - ATTN → D5 (host-sleep jumper). Fit a short jumper between the ATTN and
D5 pins on the Notecarrier CX. Both sit on the same 16-pin header (
ATTNat position 3,D5at position 14). The Notecard pullsATTNlow while the host sleeps and raises it when the next sample is due, and the rising edge onD5wakes 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 covers the full pattern. - +VBAT / +5V pad → Mojo
LOADoutput;BATinput of Mojo ← 5V DC from the AC/DC supply.
Mount the probes inside the unit. The supply thermistor goes ~12″ past the evaporator coil in the supply duct, and the return thermistor goes in the return plenum upstream of the filter. Clamp the CT on either of the hot legs feeding the compressor contactor (only one leg is required, since clamping both will cancel). Use silicone tubing to tap the SDP810 tubes across the filter frame.
Note on the CT threshold default. compressor_on_amps = 3.0 is calibrated
for typical light-commercial single-stage RTU compressors (3-to-5-ton, single
phase, drawing ~12–25 A under load). On the smallest 1.5–2-ton units, the
in-rush-then-steady-state current can hover near 4–5 A, so 3 A still works as a
safe "compressor running" floor. On larger units the threshold is conservative,
and you may want to raise it with the compressor_on_amps env var to better
separate "running" from "starting transient." See §6.
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:rtu-pdm. -
Set the ProductUID in firmware. Open
rtu_predictive_maintenance.inoand replace the empty string on the#define PRODUCT_UID ""line (around line 32) with your value. If you'd rather not edit the source, pass it as a build flag instead (-DPRODUCT_UID=\"com.your-company.your-name:rtu-pdm\"). -
Claim the Notecard. Power the assembled unit. The Notecard associates itself with your Notehub project the first time it connects over cellular, so there's no manual claim step. The device shows up in your project's Devices tab within a minute or two.
-
Create a Fleet. Fleets (and Smart Fleets) are how Notehub groups devices for config and routing. One fleet per service territory is a reasonable starting point. You can set environment variables (thresholds) at the fleet level and override them per device if a unit turns out to be unusual.
-
Set environment variables. In Notehub, go to Fleet → Environment (or Device → Environment for a one-off override) and add any of the variables below. The device picks them up after the Notecard's next sync, with no reflash and no truck roll. They're all optional, and the table shows the firmware defaults and the range each one accepts.
Variable Default Range Purpose delta_t_min_f12.01–40 Supply/return delta-T °F below which delta_t_lowfires (only evaluated while the compressor is actually running).short_cycle_starts_per_hour81–15 Compressor starts per rolling hour above which short_cyclingfires.filter_dp_alert_pa90.01–125 Filter differential pressure (Pa) above which filter_dp_highfires.compressor_on_amps3.00.5–30 Amps above which the compressor counts as running. Raise it for larger 5+ ton units. Lower it only with care, because the value also gates compressor-runtime accounting. sample_interval_sec6010–3600 Seconds between samples. summary_interval_min605–1440 Minutes between summary Notes. Changing it also re-applies hub.setso the Notecard's outbound transmit cadence tracks the new value.The firmware ignores a value that isn't a plain number or falls outside its range, and keeps its current setting, so a typo can't zero out a threshold or the sample interval. The upper limits come from the hardware. The SDP810-125Pa reads up to 125 Pa, the CT tops out at 30 A, and the start counter holds 16 starts, so a limit of 16 or more could never fire.
-
Configure routes. At a minimum, add one route for
rtu_alert.qo(to an on-call or CMMS endpoint) and a second forrtu_summary.qo(to a long-term store). Splitting the two Notefiles at the source lets each one go to its own destination at its own urgency, with no filtering in the route.
To set up SMS messaging, see the guide on configuring a Twilio route.
What you should see in Notehub
Within a minute of first power-on, events should start showing up in your project's Events tab. Three kinds matter for this project:
_session.qo: automatic Notecard housekeeping events on each cellular session. They're a handy sanity check that the radio is reaching Notehub at all.rtu_summary.qo: one persummary_interval_min. The body looks like this:Any field reading{ "supply_f": 52.4, "return_f": 71.8, "delta_t_f": 19.4, "compressor_amps": 14.7, "filter_dp_pa": 31.2, "starts": 3, "runtime_min": 41.0 }-9999means "no valid samples for this metric in the window." Treat it as a sensor fault, not a near-zero measurement.rtu_alert.qo: sent only on a threshold trip, and transmitted immediately. Thealertfield is one ofdelta_t_low,short_cycling, orfilter_dp_high, and the other fields carry the supporting numbers. §8 has the payload shape for each alert.
7. Firmware Design
The main sketch is
firmware/rtu_predictive_maintenance/rtu_predictive_maintenance.ino.
Host sleep (the card.attn request, STOP2 entry, and ATTN wake) lives in its
own file,
firmware/rtu_predictive_maintenance/cx_sleep.h.
7.1 Installing and flashing
Dependencies:
- Arduino core for STM32:
stm32duino/Arduino_Core_STM32. Install it from the Arduino Boards Manager (search "STM32 MCU based boards") or add the index URLhttps://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.jsonunder File → Preferences → Additional Boards Manager URLs. The Notecarrier CX's onboard host uses the Cygnet board definition (see the build settings below). Blues Wireless Notecardlibrary:note-arduino. Install it from the Arduino Library Manager or witharduino-cli lib install "Blues Wireless Notecard". Pin whatever current stable release the Library Manager offers when you install. The note-arduino releases page lists the latest.STM32duino Low Powerlibrary (and its dependencySTM32duino RTC):stm32duino/STM32LowPower. It provides the STOP2 entry and ATTN-pin wake thatcx_sleep.huses.
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 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).
Flashing with the Arduino IDE: Open rtu_predictive_maintenance.ino, apply
the build settings above, and hit Upload. With the Notecarrier CX's USB-C
switch in HST, the host's DFU bootloader is reachable over the USB-C port
(hold BOOT, tap RST). You can also upload over SWD with an ST-LINK on the
debug jack.
Flashing with arduino-cli: From the firmware directory, run:
arduino-cli core install STMicroelectronics:stm32
arduino-cli lib install "Blues Wireless Notecard" "STM32duino Low Power" "STM32duino RTC"
# usb=none disables the USB CDC stack, which must be off for the host to sleep.
arduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none rtu_predictive_maintenance/
arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none -p /dev/cu.usbmodem* rtu_predictive_maintenance/rtu_predictive_maintenance/sketch.yaml sets the same board options, so running
arduino-cli compile in that directory without -b picks them up too. Replace
/dev/cu.usbmodem* with whatever port the Notecarrier shows up as on your
machine. That's usually COMx on Windows and /dev/ttyACM* on Linux.
After the upload, open the ST-LINK virtual COM port at 115200 baud and watch
the [sample] lines go by. Each wake prints [sample] once and hands off to the
Notecard, and then the host drops into STOP2 until the next interval. Don't
worry when the output goes quiet for ~60 seconds at a time.
7.2 Modules
| Responsibility | Where |
|---|---|
Notecard configuration (hub.set, templates, accelerometer quiet) | hubConfigure, defineTemplates |
| Env-variable override fetch and range checks (per wake) | fetchEnvOverrides, envFloat, envUint |
| Thermistor / CT / SDP810 reading | readThermistorF, readCompressorAmps, readFilterDpPa |
| Threshold evaluation and alert emission | runSampleCycle, sendAlert |
| Hourly summary | sendSummary |
Host sleep (STOP2 until the Notecard raises ATTN) | cx_sleep.h: cxSleepBegin, cxSleepUntilAttn |
| Application state (held in RAM across sleep cycles) | AppState |
7.3 Sensor reading strategy
- Thermistors. The firmware averages 16 samples of 12-bit ADC counts, turns that into resistance using the divider ratio, gets the temperature from the β equation, and converts it to Fahrenheit.
- CT. The SCT-013-030 puts out an AC signal. A 2-resistor divider with a 10 µF capacitor biases it to Vref/2 so the ADC only sees positive voltages. The firmware gets the DC offset from a 256-sample mean, then computes RMS over ~20 mains cycles (1480 samples) and scales at 30 A per volt RMS.
- SDP810. The firmware starts I²C continuous measurement mode once at boot. Each read pulls three bytes (MSB, LSB, CRC) and divides by the 125 Pa variant's 240 count/Pa scale factor. The 500 Pa variant uses 60 count/Pa. Both variants share the same part family name, so be careful not to mix them up.
7.4 Event payload design
The firmware ships one
template-backed
summary Note (rtu_summary.qo) every hour and sends untemplated alerts
(rtu_alert.qo) immediately with sync:true. Templates matter here because an
RTU pack running for 10 years on 500 MB has to stay within its data budget.
Templated Notes travel as fixed-length records instead of free-form JSON, which
shrinks the wire size by roughly 3–5×.
The snippet below uses the Note template
format, so here's a
quick refresher. Each numeric placeholder is a magic number that declares the
field's wire type and size, and a .1 marks a float. 14.1 is a 4-byte
IEEE-754 float, 12 is a 2-byte signed integer, 11 is a 1-byte signed
integer, 18 is an 8-byte signed integer, and 18.1 is an 8-byte float. The
Notecard rejects any later note.add whose body doesn't match the registered
shape, so firmware mistakes get caught at the device instead of days later in
your downstream pipeline.
Here's an example alert body:
{
"file": "rtu_alert.qo",
"body": {
"alert": "delta_t_low",
"delta_t_f": 8.2,
"supply_f": 48.1,
"return_f": 56.3
},
"sync": true
}7.5 Low-power strategy
Even with line power available, the host stays asleep most of the time. That
means less heat in the enclosure and less wear on the supply, and the same
firmware pattern ports straight to battery- or solar-powered variants without a
rewrite. 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,
the host calls cxSleepUntilAttn() (in cx_sleep.h). That sends a
card.attn sleep
request for sample_interval_sec seconds, waits for the Notecard to pull ATTN
low, and enters STM32 STOP2, where the host draws a few microamps and keeps its
RAM. When the interval is up, the Notecard raises ATTN, the rising edge on
D5 wakes the host, and execution resumes on the next line of loop(). The
host resumes in place instead of cold-booting, so the AppState struct can just
live in RAM, and nothing gets serialized to the Notecard. If the sleep request
fails or ATTN never goes low (because the jumper is missing), loop() waits
out the interval awake and tries again on the next cycle, which keeps the sample
cadence either way.
The Notecard has its own low-power idle (~8 µA @ 5V) between cellular wakes. Sampling and transmitting run on separate clocks on purpose. The firmware samples every 60 seconds but only transmits once an hour, and alerts are the only thing that skip the transmit timer.
7.6 Retry and error handling
- Sensor reads that return
NaN(unplugged probe, ADC rail pegged) are left out of summary averages metric by metric. Each metric has its own valid-sample counter, so one bad SDP810 read doesn't bias the thermistor averages. If a metric has zero valid samples in the window, the summary emits-9999as a sentinel instead of a misleading zero. - The compressor-on branch also keeps delta-T from being evaluated when the unit isn't actually cooling, so a perfectly fine RTU idling overnight doesn't page anyone.
- A change to
summary_interval_minre-applieshub.seton the next wake, so the Notecard's outbound cellular cadence stays in step with the local summary cadence instead of drifting apart. ALERT_COOLDOWN_SEC(30 minutes) de-duplicates alerts, so a slow-drifting sensor can't page the on-call engineer on every single sample. One alert per 30 minutes per failure mode is plenty.- Alerts and summaries are each one
note.addrequest, and the firmware doesn't check whether the Notecard accepted it. If an alert is rejected, its cooldown still starts, so that alert kind stays quiet for the next 30 minutes.
7.7 Key code snippet 1: template definition
The template makes each summary Note a fixed-length record on the wire. 14.1
means a 4-byte float, and 12 means a 2-byte signed integer.
J *req = notecard.newRequest("note.template");
JAddStringToObject(req, "file", "rtu_summary.qo");
JAddNumberToObject(req, "port", 50);
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "supply_f", 14.1);
JAddNumberToObject(body, "return_f", 14.1);
JAddNumberToObject(body, "delta_t_f", 14.1);
JAddNumberToObject(body, "compressor_amps", 14.1);
JAddNumberToObject(body, "filter_dp_pa", 14.1);
JAddNumberToObject(body, "starts", 12);
JAddNumberToObject(body, "runtime_min", 14.1);
notecard.sendRequest(req);7.8 Key code snippet 2: immediate-sync alert
sync:true tells the Notecard not to wait for the next outbound window. The
Note jumps the queue, and the radio wakes right away. All three alert kinds go
through sendAlert(), which takes the alert name and up to three key/value
pairs. Here's the function, followed by the delta_t_low check in
runSampleCycle() that calls it:
static void sendAlert(const char *kind,
const char *a_key, float a,
const char *b_key, float b,
const char *c_key, float c) {
J *req = notecard.newRequest("note.add");
JAddStringToObject(req, "file", "rtu_alert.qo");
JAddBoolToObject(req, "sync", true); // alerts bypass the outbound timer
J *body = JAddObjectToObject(req, "body");
JAddStringToObject(body, "alert", kind);
if (a_key) JAddNumberToObject(body, a_key, a);
if (b_key) JAddNumberToObject(body, b_key, b);
if (c_key) JAddNumberToObject(body, c_key, c);
notecard.sendRequest(req);
}
// ...
if (compressor_on && !isnan(delta_t) && delta_t < DELTA_T_MIN_F &&
(now_epoch - state.last_alert_delta_t_epoch) > ALERT_COOLDOWN_SEC) {
sendAlert("delta_t_low",
"delta_t_f", delta_t,
"supply_f", supply,
"return_f", ret);
state.last_alert_delta_t_epoch = now_epoch;
}7.9 Key code snippet 3: sleep between samples
cxSleepUntilAttn() asks the Notecard to hold ATTN low for
SAMPLE_INTERVAL_SEC seconds, confirms the pin has gone low, and puts the STM32
into STOP2. Because the host stays alive, the card.attn call is a request
whose result gets checked, not a fire-and-forget command. The Notecard's ATTN
pin brings the host back on schedule, and loop() moves on to the next sample.
setup() arms the wake once, and every pass through loop() ends with the
sleep call. The sketch also has a version of that call without the log stream,
for builds with logging turned off.
void setup() {
// ...
// Wake from STOP2 on the ATTN rising edge.
cxSleepBegin();
}
void loop() {
// ...
runSampleCycle();
state.cycles++;
// Sleep until the Notecard raises ATTN SAMPLE_INTERVAL_SEC from now.
if (!cxSleepUntilAttn(notecard, SAMPLE_INTERVAL_SEC, NULL, &usbSerial)) {
// The Notecard didn't take the sleep request, or ATTN never went low
// (check the ATTN -> D5 jumper). Keep the sample cadence and try again.
delay(SAMPLE_INTERVAL_SEC * 1000UL);
}
}8. Data Flow
Every 60 seconds the firmware runs one sample cycle, evaluates three independent
threshold checks in parallel, and sends an alert for each one that trips. All
three alerts go to the same rtu_alert.qo Notefile with sync:true, so each
failure mode pages out no matter what state the other two are in.
-
Collected. Supply-air °F, return-air °F, compressor RMS amps, filter DP in Pa, compressor starts in the last rolling hour, and cumulative compressor runtime.
-
Transmitted.
rtu_summary.qo: one template-encoded record everysummary_interval_min(60 minutes by default). Each numeric field is the average of its valid samples over the window. If a sensor produced zero valid reads, that field carries the sentinel-9999, so downstream analytics can tell "sensor failed" apart from a real near-zero reading.rtu_alert.qo: sent only on a threshold trip and synced immediately, with a 30-minute dedup window per alert kind.
-
Routed. Both Notefiles go to Notehub, and from there to whatever downstream the project's routes specify. The two filenames are kept separate on purpose so you can send them to different places.
-
Alerts trigger on.
delta_t_low: cooling delta-T drops belowdelta_t_min_fwhile the compressor is drawing current (a refrigerant loss indicator). Body:{ alert, delta_t_f, supply_f, return_f }.short_cycling: more thanshort_cycle_starts_per_hourcompressor starts inside a true rolling 60-minute window (a ring buffer of recent start timestamps). Body:{ alert, starts_per_hour, compressor_amps }.filter_dp_high: filter DP rises abovefilter_dp_alert_pa. Body:{ alert, filter_dp_pa }.
Each alert kind has its own 30-minute cooldown (
ALERT_COOLDOWN_SEC), so a slow-drifting sensor can't page the on-call engineer every minute. The three trips are independent. A unit can be short-cycling and losing refrigerant at the same time, and you'll see one alert of each kind.
9. Validation and Testing
Expected cadence after deployment. In steady state, a healthy RTU generates
one rtu_summary.qo event per hour and no rtu_alert.qo events. Expect the
occasional alert during the first week while you dial in thresholds for that
particular unit, since thermistor placement, duct geometry, and compressor model
all shift the baseline around.
Using Mojo to validate power behavior (optional bench step). Notecard's published idle figure is roughly 8 µA @ 5V, and the low-power design guide has the authoritative measured numbers across Notecard SKUs and modes. On LTE Cat-1 bis, a transmit burst for one small queued Note lasts on the order of tens of seconds at a peak of hundreds of mA, mostly spent on radio warm-up and network registration. In STOP2 between samples, the host MCU adds only ~1–2 µA to the +VBAT rail, so the idle floor for the whole board sits around ~10 µA.
For a concrete target, the defaults in this sketch (60-second sample, hourly transmit, host in STOP2 between samples) should land on the order of 25–40 mAh / 24 h in steady state on cellular, mostly from the once-an-hour radio burst. If you're off by more than ~3×, it's usually one of two things. Either the host isn't actually sleeping (a continuous tens-of-mA baseline), or the radio is camping in registration (longer-than-expected hourly bursts on weak signal). A Mojo trace shows both before anything else does.
The Mojo reports cumulative mAh to the Notecard at 1% accuracy over its Qwiic link. A good bench exercise is to leave a unit running for 24 h on a known battery and confirm the Mojo tally matches the expected pattern: "host awake for a few seconds every minute, radio on for tens of seconds once an hour." These are the shapes to look for on a Mojo trace:
- Healthy: a flat baseline near zero, brief sub-second blips at the sample interval, and one ~10–30-second burst at 100–300 mA per hour.
- Host never sleeping: a flat, continuous baseline of tens of mA. The host
isn't entering STOP2. That's almost always a missing
ATTN→D5jumper (the firmware then waits out the interval awake and retries) or a build with USB support enabled (the host leaves STOP2 within a millisecond). Check the jumper and theusb=nonebuild option. - Weak signal / radio struggling: the hourly bursts are spaced correctly, but each one runs 60 seconds+ at peak. Move the cellular antenna or check site coverage.
The deployed RTU is grid-tied through the 120VAC service-disconnect supply, so in production the shape of the trace matters more than the absolute mAh number. The bench measurement is still worth doing once per firmware revision, because it's the cheapest way to catch a regression that quietly keeps the host awake.
10. Troubleshooting
Here's a short field guide to what actually goes wrong on first bring-up.
| Symptom | Likely cause | What to check |
|---|---|---|
| Device never appears in Notehub's Devices tab. | PRODUCT_UID is empty or wrong, or the cellular antenna is disconnected. | Double-check that PRODUCT_UID matches the Notehub project exactly. Move the unit outside or near a window. Check for _session.qo events, because if there are none, there's no cellular connection. |
Device appears, _session.qo events arrive, but no rtu_summary.qo ever shows up. | The first summary isn't due yet (the host writes it after a full summary_interval_min window, and it ships on the next outbound sync), or the host isn't running its sample cycles. | Give it up to two hours. Then open the ST-LINK virtual COM port at 115200 baud (see §7.1) and check that a [sample] line shows up every minute. Any field reading nan points to an unplugged or miswired sensor. |
| Thermistor reads pegged at one extreme. | Probe is open (reads near +3V3) or shorted (reads near 0 V). | The firmware returns NaN outside the valid divider range, so you'll see nan instead of misleading numbers. Check the divider wiring at A0 / A1. |
compressor_amps always 0 even with the compressor running. | The CT is on the neutral leg or on both hot legs (the currents cancel), or the bias network is missing the 10 µF cap. | Re-clamp the CT on one hot leg only. Check that the A2 bias node sits at ~Vref/2 (1.65 V) at idle. |
filter_dp_pa reads NaN on every sample. | SDP810 didn't ACK on I²C, or the address is wrong. | Confirm the SDP810-125Pa is at 0x25 (some part variants use 0x26). Check the pull-ups. The Notecarrier CX has them, but breakouts in line may add more. |
| An environment variable you set in Notehub has no effect. | The value isn't a plain number or is outside its range, or the Notecard hasn't synced since you set it. | Check the value against the ranges in §6. The firmware ignores out-of-range values and keeps its current setting. It reads the variables on every wake, but new values only reach the Notecard when it syncs. |
| Alerts firing constantly on a healthy unit. | Thresholds aren't tuned for this unit yet. | Lower delta_t_min_f a few °F if this unit's normal delta-T runs close to the 12 °F default (delta_t_low fires when delta-T drops below it), and watch one rtu_summary.qo per hour to see what the actual operating envelope looks like. |
| Mojo bench trace shows continuous tens of mA. | The host isn't entering STOP2, because the ATTN→D5 jumper is missing or the firmware was built with USB support enabled. | Confirm the ATTN→D5 jumper is in place (see §5) and that the build used USB support: None (usb=none). §9 shows the trace shapes. |
If your problem isn't on this list, the Blues community forum is usually the fastest place to get a second pair of eyes on a Notecard + sensor setup.
11. Limitations and Next Steps
This pack is meant as a starting point. It's small enough to retrofit, and it goes after the failure modes a single technician already knows how to spot. A handful of things are out of scope on purpose, and a longer list of extensions makes sense once the basic detector is paying for itself.
Simplified for the POC
Each item below is a deliberate trade. A fuller platform would want the capability, but skipping it keeps the pack simple enough for one technician to retrofit.
Detection is rule-based. The firmware runs three scalar threshold checks instead of a trained model. They catch the impending-failure patterns HVAC technicians already recognize on a service call, but they don't do the anomaly detection or drift analysis that a more sophisticated predictive-maintenance platform would add on top.
CT readings are single-phase only. Three-phase commercial RTUs need three CTs and a firmware change to sum them.
The SDP810 CRC byte is read and discarded rather than verified, so a communication-layer fault on the I²C bus won't get caught.
Compressor-start detection is sampled, not interrupt-driven. Transitions
that start and finish entirely inside one sample_interval_sec window won't be
counted. The 60-second default is well below normal RTU cycle times, but a badly
misbehaving contactor could, in principle, cycle faster than the sampler can
see.
No local storage of historical samples. The Notecard keeps its own multi-day Note queue, but the sketch itself holds only the current summary window in RAM, plus a 16-slot ring buffer of recent compressor starts.
The 120VAC power path assumes the installer can tap the RTU service disconnect. If an installation can only offer the 24VAC control-transformer rail, it needs a supply that takes 24VAC in and puts 5V DC out, such as a bridge rectifier feeding a wide-input DC-DC converter rated well above the ~34V peak of rectified 24VAC. The downstream 5V wiring doesn't change. For units where neither rail is practical, a Scoop / solar variant would be a reasonable addition.
Mojo is bench-validation equipment in this POC. The firmware doesn't read its LTC2959 coulomb counter over the Qwiic bus. If you want fleet-level power telemetry, adding a runtime mAh field to the summary is a straightforward extension.
Production Next Steps
Once the basic detector is paying for itself, these extensions are worth adding. They're ordered roughly from most immediately useful to most infrastructure-dependent.
Three-phase CT support (or 208/240V split-phase) brings larger units into scope with a second and third CT on A3/A4 and an RMS sum in firmware.
A duct-static-pressure sensor on the supply side would tell a "clogged filter" apart from a "blocked coil." From the return side alone the two look alike, but a clogged coil shows rising static on both sides, while a clogged filter only rises on the return.
A CRC check on all SDP810 I²C reads is an easy win. The bytes are already in the frame, and the firmware just ignores them today.
Field-upgradeable firmware via Notecard Outboard DFU would let a service company push a threshold recipe or a new failure-mode detector to the whole fleet without a truck roll.
Per-unit commissioning with a probe-offset correction environment variable tightens accuracy. Thermistors drift a few degrees with duct placement, and calibrating once at install time beats living with the noise.
A weather-input overlay correlates delta-T against outside-air temperature, either through a Notehub environment variable fed by a weather API route or from a dedicated outdoor-air probe. That keeps refrigerant-loss detection from false-triggering during mild-weather startup cycles, when delta-T naturally runs low before the coil saturates.
12. Summary
The service company that used to learn about a failed RTU from an angry grocer at 2 AM now gets paged hours earlier about refrigerant slowly leaking, a contactor starting to chatter, or a filter that's close to clogging. Two thermistors, a clamp-on CT, an I²C pressure sensor, and a cellular Notecard turn an opaque rooftop box into an asset that tells on itself before tenants notice. The same firmware drops onto the next rooftop, and the one after that, without an IT ticket or a site visit.