Regulatory-Grade Pharmacy and Lab Cold Storage Audit 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/cold_storage_audit_monitor/ so it can learn how the device works and help you adapt it to your hardware.
This project is a safety assurance reference design for pharmacies, clinical laboratories, and vaccine depots. It keeps a continuous, automatically timestamped temperature record for every refrigerator and freezer in their compliance scope, and it delivers that record over a cellular data path that bypasses the facility's regulated network entirely. Immediate alerts fire as soon as a sample finds the temperature outside its configured range or a door left open too long.
1. Project Overview
The problem. Pharmacies, clinical laboratories, and vaccine depots answer to a patchwork of overlapping regulations. There's USP Chapter 659 (Packaging and Storage Requirements), FDA 21 CFR Part 211.68, state board-of-pharmacy rules, and, for federally funded programs, the CDC Vaccine Storage and Handling guidelines. Every one of those frameworks requires automated temperature records with defined excursion thresholds. For most refrigerated vaccines and biologics that's 2°C–8°C, with documentation of any deviation, its duration, and the corrective action taken.
Facilities have plenty of refrigerators. What most of them lack is automated monitoring. A datalogger someone has to download by hand, a wall thermometer read once per shift, and a WiFi-connected sensor that goes dark whenever the facility's network hiccups all fall short under a regulatory audit. Automated monitoring with individually timestamped readings and immediate excursion alerts is far more defensible than manual spot checks, and it produces a record that's complete even when nobody was watching.
This reference design shows how to close that gap. Every five minutes, the Notecarrier CX's onboard STM32 host wakes to read a high-precision digital temperature sensor, check a magnetic door switch, and cross-check both readings against an ambient-light sensor at the door frame. Each wake produces one timestamped reading Note that's queued in the Notecard's on-device store, and the queue flushes to the Blues Notehub cloud service on the scheduled cellular connection. If the temperature strays outside its configured limits, or the door stays open past an acceptable threshold, an alert Note goes out immediately. It skips the batched outbound window and lands in whatever on-call or compliance system the operator routes it to.
Why Notecard. Pharmacies and clinical labs run tightly managed networks. PCI-compliant retail pharmacy networks, HIPAA-covered clinical networks, and federally regulated vaccine storage programs all have strict rules against unknown IoT-class devices on the primary LAN. A temperature sensor on the pharmacy WiFi will either fail a network security review or get quietly firewalled off from the internet. An independent cellular data path avoids that whole conversation. The Notecard registers directly on the carrier's network and never touches the facility's LAN, so the IT team never has to issue a network access request or sign a BAA for a refrigerator.
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.
That independence matters for continuity too. Facility WiFi goes down for maintenance, storms, and power events, sometimes for hours, and a WiFi-dependent compliance monitor is exactly the kind of device that silently stops logging at the worst possible moment. Notecard's store-and-forward queue buffers Notes locally through a connectivity gap and syncs them, timestamps intact, when the cellular session resumes. The audit-evidence record this design produces has two streams: per-sample reading Notes, one timestamped Note per 5-minute wake queued individually in the Notecard's flash-backed store, and immediate alert Notes, sent the moment a threshold is tripped regardless of the scheduled sync cadence. Every reading is persisted as its own Note in the Notecard's on-device flash queue, with no aggregation. That preserves sample lineage across cellular connectivity gaps, so an auditor can reconstruct the exact temperature history and door-event timeline for any window. An auditor reviewing a weekend excursion has nothing to go on but that buffered record: every individual sample, plus any alert Notes that fired. (This store-and-forward guarantee covers cellular and WiFi outages. A separate, shallower host-side retry ring handles the different case where the host can't reach the Notecard over I²C. See §7 Retry and error handling and Limitations.)
WiFi fallback on the MBGLW is available as a secondary path, but only for sites that provide an explicitly approved, segregated IoT network for the device. Putting it on the facility's primary pharmacy or clinical LAN defeats the network independence this design depends on. A compliance monitor that sits behind a firewall exception is one network policy change away from silent failure.
Deployment scenario. A small weatherproof enclosure mounts on the exterior of the cold storage unit, never inside the refrigerated compartment. Sustained cold and condensation will damage unprotected electronics, and a metal refrigerator body will block cellular signal. The Adafruit MAX31865 amplifier board mounts inside the enclosure. The Adafruit PT1000 probe cable exits through a cable gland and routes into the compartment through the cabinet's manufacturer-provided probe port or a door-gasket pass-through (see §5 Wiring and Assembly), which puts the stainless-steel probe capsule at the geometric center of the storage volume. The VEML7700 light sensor mounts on the outside of the door frame, out of the cold zone, where it detects light spilling out when the door is ajar. Limitations covers condensation-tolerant placement options for production. The two halves of the door switch mount on the door and the frame. The Notecarrier CX sits in the enclosure, powered over USB-C from a wall adapter, and the cellular antenna mounts on the outside of the enclosure where it has line of sight to the network. Nobody has to configure a network, file an IT ticket, or download data by hand. For bench development without a probe routed into a cabinet, a TMP117 breakout (bench library-swap required) mounted inside the enclosure measures exterior ambient air and lets you validate the firmware architecture and cellular data path.
2. System Architecture
Device-side responsibilities. Every five minutes the Notecarrier CX's STM32
host wakes and takes a single coordinated snapshot: the MAX31865 over SPI, the
VEML7700 over I²C, and the reed switch from a GPIO. It runs the four alert rules
against that snapshot and goes back to sleep. Between samples the host sleeps in
STM32 STOP2 (a few µA, RAM retained) until the Notecard's ATTN pin wakes it. The
firmware asks the Notecard to hold ATTN low for the sample interval via
card.attn sleep mode,
and the rising edge on the jumpered host pin brings the host back. Anything the
firmware needs to remember across that sleep, like the door-open timestamp the
compliance officer cares about and the per-alert cooldown timers, just stays in
the AppState struct in RAM. Nothing is serialized to the Notecard.
Notecard responsibilities. The Notecard has three jobs that together produce
the audit-evidence record. It queues Notes in
on-device flash and flushes them on the configured
hub.set outbound cadence
(60 minutes by default), although any sync:true alert Note skips that schedule
and opens a session immediately. Its real-time clock stays synchronized to UTC
through Notehub. That clock supplies the epoch timestamps the firmware needs for
door-duration tracking and alert cooldowns, and the Notecard stamps every Note's
when field with its UTC value at enqueue time. (Notes queued before the first
successful Notehub session carry an unverified RTC timestamp and aren't
audit-grade. See Limitations.) Finally, it
distributes environment
variables
from Notehub, so a compliance officer can retune thresholds and door timeouts
from a browser without anyone reflashing firmware.
Notehub responsibilities. The Notecard's embedded global SIM puts the device on carrier cellular networks worldwide, and its data reaches Notehub over the Internet. Notehub stores every event and applies project-level routes. Reading Notes and alert Notes land in separate Notefiles, so the long-term compliance archive and the on-call pager each get exactly the volume they need, with no filtering logic in the route. Smart Fleets let a multi-site operator group units by storage class (refrigerator vs. freezer vs. ultra-cold) and push the matching threshold preset to the whole class at once.
Routing to the cloud (high level only). Notehub can route to HTTP, MQTT, AWS, Azure, GCP, Snowflake, and several other destinations. Route setup depends on your project, so see the Notehub routing docs for details. This project doesn't ship any specific downstream endpoint.
3. Technical Summary
What you'll have when done: A Notecarrier CX with a calibrated PT1000 temperature probe, door switch, and light sensor. It sends a timestamped reading to Notehub every 5 minutes and alerts immediately on temperature excursions or prolonged door-open events. Readings collect in the Notecard's flash-backed queue and sync on a 60-minute cellular schedule, completely independent of facility WiFi.
Fastest path to first event (no probe):
- Get a Notecarrier CX + MBGLW, a VEML7700 sensor, and a magnetic door switch
- Wire the two sensors (the VEML7700 on Qwiic and the door switch on a GPIO, as shown in §5) and fit the ATTN-to-D6 jumper on the Notecarrier CX header
- Clone this repo and paste your Notehub ProductUID into
firmware/cold_storage_audit_monitor/cold_storage_audit_monitor.ino(line 67) - Install the libraries
(
arduino-cli lib install "Blues Wireless Notecard" "STM32duino Low Power" "STM32duino RTC" "Adafruit MAX31865 library" "Adafruit VEML7700 Library") and flash witharduino-cli compile -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none firmware/cold_storage_audit_monitor/ && arduino-cli upload -b STMicroelectronics:stm32:Blues:pnum=CYGNET,usb=none -p /dev/ttyACM0 firmware/cold_storage_audit_monitor/(adjust the port for your OS). Theusb=noneboard option turns off the USB CDC stack, which has to be off for the host to sleep (see §7). These board options matchfirmware/cold_storage_audit_monitor/sketch.yaml, so you can also leave out-bwhen you run from the sketch directory - Power up and check that readings appear in Notehub. They're queued and flushed on the 60-minute outbound schedule, so the first batch can take up to an hour (plus 1–5 minutes on first power for cellular registration)
- Override thresholds in Notehub Fleet → Environment (e.g.,
temp_high_alert_c: 8.0,temp_low_alert_c: 2.0for refrigerated storage)
For production: Follow §11 and get a NIST-traceable calibration certificate for the PT1000 probe assembly before regulatory deployment.
Here's a sample Note from this device:
{
"file": "storage_reading.qo",
"body": {
"temp_c": 4.62,
"lux": 0.18,
"door_open": false,
"door_open_sec": 0,
"sample_epoch": 1714435200,
"time_valid": true,
"dropped_readings": 0,
"dropped_alerts": 0
}
}4. Hardware Requirements
| Part | Qty | Rationale |
|---|---|---|
| Notecarrier CX | 1 | Integrated carrier with an onboard STM32L433 host MCU, so you don't need a separate MCU. The host sleeps in STOP2 between samples, and the Notecard's ATTN pin wakes it. |
| Notecard Cell+WiFi (MBGLW) (datasheet) | 1 | Cellular keeps the monitor on an independent data path, isolated from the facility's regulated network segments. WiFi is available as a fallback only at sites that provide an explicitly approved, segregated IoT network. The facility's primary pharmacy or clinical LAN is not an appropriate fallback. |
| Blues Mojo | 1 | Coulomb counter for validating current draw on the bench. It sits inline on the +VBAT rail during development and isn't deployed in the enclosure. |
| Adafruit Platinum RTD Sensor, PT1000, 3-Wire, 1 m (Product 3984) | 1 | Production temperature probe. 316L stainless-steel capsule (4 mm × ~30 mm) on a 1 m cable with three bare wire leads that connect to the MAX31865 terminal blocks. Its operating range of −50°C to 280°C covers refrigerated (2–8°C) and standard freezer storage (down to approximately −50°C). This probe does not cover ultra-cold (−80°C) storage. That range requires a dedicated RTD probe rated to −80°C or below, which is outside the scope of this reference design. The probe capsule routes into the refrigerated compartment, and the cable passes through a dedicated cable gland in the enclosure wall. Before regulatory deployment, submit this specific probe assembly to an accredited calibration laboratory for a NIST-traceable calibration certificate. The calibration applies to the individual probe unit, so keep the certificate on file with that unit's commissioning documentation. |
| Adafruit PT1000 RTD Temperature Sensor Amplifier, MAX31865 (Product 3648) | 1 | Production RTD amplifier. Connects the 3-wire PT1000 probe to the Notecarrier CX over hardware SPI (CS on D10). It has an onboard 4300 Ω reference resistor and 3.3 V regulator and is 5 V tolerant. Mounts inside the electronics enclosure. Set the 2/3-wire solder jumper on the bottom of the board for 3-wire mode before wiring. The firmware uses rtdAmp.begin(MAX31865_3WIRE) and rtdAmp.temperature(1000, 4300.0). |
| SparkFun TMP117 High Precision Temperature Sensor, Qwiic (SEN-15805) | 1 | Bench substitute only, not the production sensor path. ±0.1°C accuracy, I²C via Qwiic. To use the TMP117, swap the Adafruit_MAX31865 library for SparkFun_TMP117 and replace readTemperatureC() in firmware/cold_storage_audit_monitor/cold_storage_audit_monitor_helpers.cpp. It mounts inside the enclosure and measures exterior ambient air only. It does not ship with a NIST-traceable calibration certificate and is not a deployable compliance instrument. |
| Adafruit VEML7700 Lux Sensor, STEMMA QT / Qwiic (Product 4162) | 1 | I²C ambient-light sensor, 0–120,000 lux range. In the production build it connects directly to the Notecarrier CX Qwiic port (no TMP117 in between). Positioned near the door opening, it detects the interior lamp independently of the door switch. That gives a second line of evidence for door-open events and flags stuck-closed switch states when light and switch disagree. Its STEMMA QT connector is Qwiic-compatible. Bench evaluation only. The unprotected breakout PCB is not suitable for permanent placement in a refrigerating or condensing environment (see Limitations). |
| SparkFun Magnetic Contact Switch Set (COM-13247) | 1 | Normally-Open (NO) reed switch assembly, with the magnet on the door and the switch body on the frame. It's mechanically simple, uses no power, and connects directly to a digital GPIO with a pull-up, so no ADC or signal conditioning is required. |
| Cellular Antenna, 698–2700 MHz, SMA (SparkFun CEL-16432) | 1 | Wideband external antenna covering the LTE Cat-1 bis frequency range the MBGLW uses in North American and global deployments, including the 700 MHz band group (LTE Bands 12/13/17) that many carriers use for LTE Cat-1 bis. It must be routed outside the enclosure, because a metal insulated enclosure or the body of an adjacent refrigerator will heavily attenuate an internal antenna and can keep the Notecard from registering on the network. Connects to the SMA-to-u.FL adapter cable below. |
| SMA to u.FL RF Adapter Cable (Adafruit 851) | 1 | Connects the Notecarrier CX CELL u.FL port to the SMA external antenna. Route the cable through a cable gland in the enclosure wall. |
| Female-to-female jumper wires, 150 mm, ×6 (available from any electronics distributor) | 1 set | Connects the Adafruit MAX31865 board to the Notecarrier CX dual 16-pin header for the SPI bus (VIN, GND, CLK, SDI, SDO, CS, six wires total). The MAX31865 has 0.1" male header pins, and the Notecarrier CX has a standard 0.1" dual-row header. |
| Qwiic cable, 500 mm (e.g. SparkFun PRT-14429) | 1 | Routes from the Notecarrier CX Qwiic port through the enclosure wall cable gland to the VEML7700 positioned near the door opening. In the production build the VEML7700 connects directly to the Notecarrier CX Qwiic port, with no TMP117 in the I²C chain. Depending on enclosure placement and door geometry, you may need a longer cable. |
| Qwiic cable, 100 mm (e.g. SparkFun PRT-14427) | 1 | Bench validation only (Mojo connection). Daisy-chains the Mojo Qwiic port from the VEML7700 Qwiic OUT connector during +VBAT bench power validation, extending the I²C bus to the coulomb counter. It isn't installed in the deployed enclosure. |
| Nylon cable glands for small cable, e.g. M12 (about 3–6.5 mm cable OD) | 3 | Weatherproof strain-relief pass-throughs for the enclosure wall: one for the antenna SMA adapter, one for the PT1000 probe cable, and one for the VEML7700 Qwiic cable. Size each gland to the cable it seals. A typical M16 gland clamps about 5–10 mm, so it won't seal on the ~3 mm Qwiic cable. The Adafruit PT1000 probe cable is 2.8 mm OD, just under a standard M12 gland's 3 mm minimum, so use a gland or sealing insert rated for smaller cable there. |
| USB-C Power Supply, 5.1V 3A (Adafruit 4298) | 1 | UL-listed, regulated 5.1V USB-C wall adapter for the Notecarrier CX. |
| Weatherproof enclosure, ~6×4×2 in (e.g. Hammond 1554C2BK or equivalent) | 1 | Protects the electronics at the exterior of the cold-storage unit, with a cable gland for the antenna lead and sensor cables. |
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
Notecard installation:
- Seat the Notecard Cell+WiFi (MBGLW) in the M.2 slot on the Notecarrier CX. The card goes in at a shallow angle and clicks flat, and then the retaining screw locks it down.
- Connect the SMA-to-u.FL adapter cable (Adafruit 851) to the CELL u.FL port on the Notecarrier CX. This is the primary cellular antenna connection, so don't confuse it with any GPS u.FL port.
- Route the adapter cable's SMA end through a cable gland in the enclosure wall. Screw the external SMA antenna (SparkFun CEL-16432) onto the SMA bulkhead on the outside of the enclosure.
- ATTN-to-D6 jumper (host sleep). Fit a short jumper between the ATTN
and D6 pins on the Notecarrier CX header. Both pins are on the same
16-pin header. (
D5is taken by the door switch andD10by the MAX31865 chip-select, so this build setsCX_ATTN_PINtoD6.) The Notecard pullsATTNlow while the host sleeps and raises it when the next sample is due, and the rising edge onD6wakes 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 just putting the host to sleep. See Putting a Host to Sleep Between Sensor Readings for the full pattern.
Enclosure location:
Mount the electronics enclosure on the exterior of the cold-storage unit, never inside the refrigerated compartment, for two reasons. A metal insulated refrigerator body or nearby metal surfaces will heavily attenuate the cellular radio signal, which can prevent network registration entirely. And sustained condensation inside a refrigerated metal box will eventually damage unprotected electronics. A sealed weatherproof enclosure (e.g. Hammond 1554C2BK) mounted on the outside of the cabinet, with sensor cables entering through cable glands, keeps the radio in ambient air where it can see the network.
The VEML7700 connects to the Notecarrier CX over Qwiic (JST-SH 4-pin: 3.3 V, GND, SDA, SCL) with onboard pull-up resistors. The MAX31865 RTD amplifier uses the SPI bus on the dual 16-pin header (see RTD temperature amplifier). The door switch only needs a digital GPIO and GND from that header, and the ATTN-to-D6 jumper is the only other header connection.
I²C sensor chain:
- Notecarrier CX Qwiic connector → 500 mm Qwiic cable → VEML7700 Qwiic IN. The cable exits the enclosure through a dedicated cable gland and puts the VEML7700 near the outside of the door opening, where it can detect the interior lamp when the door is open. In the production build, the VEML7700 connects directly to the Notecarrier CX Qwiic port, with no TMP117 in the I²C chain. The MAX31865 RTD amplifier connects over SPI (see RTD temperature amplifier below). Do not permanently mount the bare VEML7700 PCB inside the refrigerated compartment. See Light sensor placement and Limitations.
- The VEML7700 runs from the Qwiic 3.3 V rail, so it needs no extra power wiring.
- VEML7700 I²C address: 0x10 (fixed).
Door switch:
- Reed switch terminal A → D5 on the Notecarrier CX dual 16-pin header.
- Reed switch terminal B → GND on the Notecarrier CX header (adjacent GND pin).
- The firmware enables
INPUT_PULLUPon D5. With the door closed (magnet present), the reed contacts close and D5 is pulled LOW. With the door open (magnet removed), the reed contacts open and the pull-up drives D5 HIGH. - Mount the magnet half on the door and the switch half on the frame within 20 mm of each other (COM-13247 rated operating gap: 20 ± 5 mm).
RTD temperature amplifier (MAX31865)
The Adafruit MAX31865 (Product 3648) mounts inside the enclosure and connects to the Notecarrier CX dual 16-pin header with six female-to-female jumper wires on the hardware SPI bus:
| MAX31865 pin | Notecarrier CX header pin | Notes |
|---|---|---|
| Vin | +3V3_OUT | 3.3 V supply |
| GND | GND | Common ground |
| CLK | SCK | SPI clock |
| SDI | MISO (silkscreen label) | On Notecarrier CX the MOSI and MISO silkscreen labels are swapped, so the pin labeled MISO on the board is the actual master-out (MOSI) line. Connect MAX31865 SDI (data in to the chip) here. |
| SDO | MOSI (silkscreen label) | The pin labeled MOSI on the board is the actual master-in (MISO) line. Connect MAX31865 SDO (data out from the chip) here. |
| CS | D10 | Software chip-select |
Notecarrier CX label swap. The MOSI and MISO pin labels are transposed on the CX board silkscreen (see Notecarrier CX datasheet). The table above gives the correct physical connections. The Arduino STM32 SPI library drives the correct hardware-peripheral lines no matter what the silkscreen says. The swap only affects how you run the jumper wires.
PT1000 probe connection. The Adafruit PT1000 probe (Product 3984) ends in
three bare wires. Before wiring, close the 2/3-wire solder jumper on the bottom
of the MAX31865 board for 3-wire RTD mode, following the Adafruit
MAX31865
guide. Then
connect the wires to the MAX31865 screw terminal blocks the way that guide
describes for a 3-wire PT1000. The firmware calls
rtdAmp.begin(MAX31865_3WIRE) at startup to match.
Route the PT1000 probe cable from the MAX31865 terminal blocks inside the enclosure out through a dedicated cable gland in the enclosure wall, then into the refrigerated compartment through the cabinet's probe port or door-gasket pass-through (see Temperature sensor placement below). The 316L stainless-steel capsule at the end of the 1 m cable is rated for −50°C to 280°C, and it's the part that enters the cold zone.
Temperature sensor placement
A probe assembly has a cable-mounted sensing element, usually a stainless-steel tip on a flexible lead, that routes from the enclosure into the refrigerated compartment by one of these paths:
- Preferred: manufacturer probe port. Many pharmacy-grade and laboratory refrigerators/freezers have a factory-drilled, gasketed probe port (usually a rubber plug or compression fitting in the cabinet wall) made specifically for external sensor cables. Check the equipment manual and use this port if there is one.
- Alternative: door-gasket dress. Route the probe lead through the hinge-side corner of the door seal gasket, where the seal compresses least and cable crush is minimal. After routing, verify that the door closes and latches normally.
Position the probe tip at the geometric center of the compartment, away from air vents and door seals, following CDC and USP 659 sensor placement recommendations.
Do not drill or punch through the cabinet wall or door. Cold-storage cabinets contain refrigerant lines and sealed foam insulation, and you can't see where they are from the outside. An unauthorized penetration can sever a refrigerant line, compromise the insulation envelope, create a condensation path into the electronics, and void the equipment's safety approvals.
Bench substitute. For firmware development and testing, you can connect the
SparkFun TMP117 breakout (SEN-15805) to the Notecarrier CX with a 100 mm Qwiic
cable. Using the TMP117 means replacing #include <Adafruit_MAX31865.h> with
#include <SparkFun_TMP117.h>, changing the rtdAmp global to a
TMP117 tempSensor object, and replacing readTemperatureC() in
firmware/cold_storage_audit_monitor/cold_storage_audit_monitor_helpers.cpp
with the TMP117 dataReady() / readTempC() poll-based implementation. The
TMP117 breakout mounts inside the enclosure and measures exterior ambient air
only. It is not a cable-mounted probe and does not measure the compartment's
interior temperature. It's appropriate only for development and the functional
validation described in Validation and Testing.
Light sensor placement
- For bench evaluation, position the VEML7700 outside the door opening, near the hinge-side door edge or the door frame, with its sensing window facing the interior lamp. There the sensor is out of the cold zone and isn't exposed to sustained condensation. Do not permanently mount the bare VEML7700 PCB inside the refrigerated compartment. Limitations covers placement options for production.
- Only use the
sensor_disagreementrule on units with a door-actuated interior lamp (the lamp turns on when the door opens and off when it closes). The rule fires when lux exceedsdoor_lux_thresholdwhile the door switch reads CLOSED. It catches a stuck-closed or failed-closed reed switch. It does not catch a missing magnet or disconnected switch, because those drive D5 HIGH and makedoor_open = true, which doesn't meet the alert condition. Do not use this rule on always-on-lamp units without raisingdoor_lux_thresholdto120000.0in Notehub (see Limitations).
Power:
- USB-C wall adapter → Notecarrier CX USB-C port (normal bench and deployment use).
- For Mojo bench validation, use a bench power supply (3.7–4.2 V, LiPo-range) as the source. Do not connect the USB-C cable during this measurement. With VUSB absent, the Notecard enters its deepest idle state and the µA-level idle figures show up on the VBAT rail. Connect the supply positive to Mojo BAT+ and run Mojo LOAD+ to the Notecarrier CX +VBAT pad on the dual 16-pin header, then return the negative rail from Notecarrier CX GND to the supply. Connect Mojo's Qwiic port to the VEML7700 Qwiic OUT connector with the 100 mm bench Qwiic cable listed in §4 Hardware Requirements, which daisy-chains Mojo onto the end of the I²C bus. This setup measures the total VBAT rail current (the Notecard plus the host when it's active) and shows the classic sleep-wake-cellular current profile described in Validation.
6. Notehub Setup
-
Create a project. Sign up at notehub.io and create a project. Copy the ProductUID and paste it into
firmware/cold_storage_audit_monitor/cold_storage_audit_monitor.inoline 67 asPRODUCT_UID. -
Claim the Notecard. Power up the unit. On its first cellular session, the Notecard associates itself with your project. Check the Notehub Events tab to confirm the device has synced.
-
Create Fleets. Fleets group devices for shared configuration and routing. A practical starting structure for cold-chain monitoring is one fleet per storage class:
refrigerated: 2°C to 8°C (vaccines, biologics)frozen: −25°C to −10°C (certain vaccines, reagents)ambient: 15°C to 30°C (room-temperature drugs)
Apply the right temperature thresholds at the fleet level with environment variables, so a single firmware image serves every storage class without recompiling. Use Smart Fleets to auto-assign devices based on a device-level environment variable (e.g.,
storage_class). -
Set environment variables. In Notehub, go to Fleet → Environment. Every variable below is optional, and the table shows the firmware defaults. Any variable you set in Notehub overrides the compile-time default on the device's next inbound sync, with no reflash required. A value that isn't a plain number, or that falls outside the range in the table, is ignored and the current setting stays in place. The two temperature thresholds are applied as a pair, so an update that would leave
temp_low_alert_cat or abovetemp_high_alert_cis ignored too.Bench vs. production defaults. The compile-time defaults (
temp_high_alert_c = 30.0,temp_low_alert_c = 15.0) are sized for the bench-mounted exterior temperature sensor, which reads room temperature, so a bench unit at ~22 °C will produce zero temperature alerts with them. For production refrigerated storage (2–8°C range per USP 659), override the thresholds in the Fleet Environment variables as shown in the table below. You don't need to reflash.Variable Bench default Production (refrigerated) Purpose temp_high_alert_c30.08.0Temperature (°C) above which temp_excursion_highfires. Set to8.0for USP 659 refrigerated storage (vaccines, biologics). Must be between −60 and 120.temp_low_alert_c15.02.0Temperature (°C) below which temp_excursion_lowfires. Set to2.0for USP 659 refrigerated storage. Must be between −60 and 120.door_open_alert_min1010Minutes a door must be continuously open before door_open_timeoutfires. Range 1–1440.alert_cooldown_min3030Minimum minutes between successive alerts of the same type. Prevents alarm fatigue during a slow-developing excursion. Range 1–1440. sample_interval_sec300300Seconds between sensor readings. Range 60–3600. Lowering this value raises per-sample Note volume proportionally. At 5 minutes (300 seconds), one bench unit produces ~288 reading Notes per day. door_lux_threshold5.05.0Lux value above which the interior counts as lit for the sensor_disagreementrule. Range 0–200000. Set to120000.0to disable the rule without a firmware rebuild. That's required on lamp-free units and on always-on-lamp units where the rule causes persistent false positives.Example: To set
temp_high_alert_cto8.0in the Fleet Environment, click + Add and enter:Key: temp_high_alert_c Type: Number Value: 8.0 -
Configure routes. In Notehub, add one route for
storage_alert.qo(to an on-call paging system, LIMS, or compliance inbox) and a second forstorage_reading.qo(to a long-term time-series archive or regulatory data repository). Eachstorage_reading.qoNote represents one 5-minute sample. At the default interval that's 288 Notes per device per day, each individually timestamped. Splitting the two Notefiles at the source lets each one go to its own destination at its own urgency, with no filter logic in the route.Example Notehub output (from the Events tab,
storage_reading.qo):{ "file": "storage_reading.qo", "body": { "temp_c": 4.62, "lux": 0.18, "door_open": false, "door_open_sec": 0, "sample_epoch": 1714435200, "time_valid": true, "dropped_readings": 0, "dropped_alerts": 0 } }Example alert (
storage_alert.qoon excursion):{ "file": "storage_alert.qo", "body": { "alert": "temp_excursion_high", "temp_c": 9.2, "lux": 0.12, "door_open": false, "door_open_sec": 0, "time_valid": true, "event_epoch": 1714435200 } }
7. Firmware Design
The firmware is small enough to read end to end, but auditors care about every
branch. So the sketch is split across four files that keep the sample cycle, the
helpers, the shared state, and the host-sleep helper cleanly separated. All four
live in firmware/cold_storage_audit_monitor/:
| File | Contents |
|---|---|
firmware/cold_storage_audit_monitor/cold_storage_audit_monitor.ino | Entry points (setup, loop), Notecard configuration, template definition, and the per-wake sample cycle |
firmware/cold_storage_audit_monitor/cold_storage_audit_monitor_helpers.h | AppState struct, shared #define constants, extern globals, and helper function prototypes |
firmware/cold_storage_audit_monitor/cold_storage_audit_monitor_helpers.cpp | Sensor reads, env-var parsing, sendReading, sendAlert, and goToSleep implementations |
firmware/cold_storage_audit_monitor/cx_sleep.h | Host sleep: card.attn request, STOP2 entry, ATTN wake |
Dependencies:
- Arduino core for STM32
(
stm32duino/Arduino_Core_STM32). Blues Wireless Notecard(thenote-arduinolibrary). Install it from the Arduino Library Manager or witharduino-cli lib install "Blues Wireless Notecard".Adafruit MAX31865≥ v1.1.0 (returnsboolfrombegin()). Install it from the Library Manager by searching "Adafruit MAX31865". Requires Adafruit BusIO.Adafruit VEML7700. Install it from the Library Manager by searching "Adafruit VEML7700". Requires Adafruit BusIO.STM32duino Low Power(and its dependencySTM32duino RTC), installed from the Library Manager. It provides the STOP2 entry and ATTN-pin wake used incx_sleep.h.
Build settings: The Notecarrier CX's host is the same STM32L433 as the
Blues Cygnet, so 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 (battery power, or the Notecarrier CX USB-C
switch in NC), the STM32's USB wakeup interrupt fires continuously and the
host never stays in STOP2. Debug output (when ENABLE_DEBUG is set to 1 in
cold_storage_audit_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 | Function | File |
|---|---|---|
Notecard configuration (hub.set, motion-mode quiet) | notecardConfigure | .ino |
| Notefile template definition (power-up, retried per wake until confirmed) | defineTemplates | .ino |
| Threshold evaluation, door-state machine, alert emission | runSampleCycle | .ino |
| Environment-variable fetch (every wake) | fetchEnvOverrides | _helpers.cpp |
| MAX31865 (PT1000) / VEML7700 / reed switch reads | readTemperatureC, readLightLux, readDoorOpen | _helpers.cpp |
| UTC epoch from Notecard RTC | getEpochTime | _helpers.cpp |
| Per-sample reading Note | sendReading | _helpers.cpp |
| Immediate-sync alert Note | sendAlert | _helpers.cpp |
Host sleep (STOP2 until the Notecard raises ATTN) | goToSleep → cxSleepUntilAttn | _helpers.cpp / cx_sleep.h |
| Application state (held in RAM across sleep cycles) | AppState | _helpers.h |
Sensor reading strategy
-
MAX31865 (PT1000). The MAX31865 runs in continuous-conversion mode from
begin()onward, so nodataReady()poll is needed. EachreadTemperatureC()call invokesrtdAmp.temperature(1000, 4300.0)(PT1000: R₀ = 1000 Ω; Adafruit board Rref = 4300 Ω) and then immediately checksreadFault(). A non-zero fault byte (RTD open, short-to-VCC, short-to-GND, or over/under-voltage) clears the fault register and returnsNAN. The reading Note'stemp_cfield then carries the−9999sentinel, so downstream analytics can tell a probe failure from a legitimate near-zero temperature reading. Values outside −60°C to 120°C are also rejected as out of range and produce the same sentinel. -
VEML7700.
readLux()uses the fixed gain (VEML7700_GAIN_1) and integration time (VEML7700_IT_100MS) set once insetup(), which gives a consistent lux reading across the 0–1000 lux range typical of a cold-storage interior (nearly dark when closed, 50–500 lux under the interior lamp when open). A negative return means a communication fault and producesNAN, and the reading Note'sluxfield carries the sentinel−1.0for that faulted sample. Lux is always ≥ 0 in normal operation, so −1.0 is unambiguously a fault marker. -
Reed switch. A 10 millisecond software debounce (two reads separated by a short delay) keeps mechanical contact bounce from registering as a spurious door event.
Event payload design
Every 5-minute wake enqueues one template-backed reading Note
(storage_reading.qo). Alert Notes (storage_alert.qo) are untemplated and go
out immediately via sync:true. The template compresses each reading to a
fixed-length binary record on the wire.
Here's a sample reading Note body as it appears in Notehub after the cellular session. All eight template fields are always present:
{
"file": "storage_reading.qo",
"body": {
"temp_c": 4.62,
"lux": 0.18,
"door_open": false,
"door_open_sec": 0,
"sample_epoch": 1714435200,
"time_valid": true,
"dropped_readings": 0,
"dropped_alerts": 0
}
}sample_epoch is the UTC epoch captured at sensor-read time. It's preserved
through retries, so a retried Note always carries the original sample timestamp
in its body even though the Notecard envelope reflects the retry time.
time_valid is false on samples taken before the Notecard RTC has synced with
Notehub. dropped_readings and dropped_alerts count host-side ring-buffer
overflows since the last successful reading Note. They count readings or
alerts that couldn't be enqueued into the Notecard over I²C, not cellular
outages, which the Notecard's
on-device queue handles transparently. Both counters reset to 0 after each
successful storage_reading.qo enqueue. A non-zero value in Notehub means
entries were dropped because the host couldn't reach the Notecard for more than
four consecutive wakes.
Here's a sample alert Note body for a temperature excursion on a refrigerated
unit with temp_high_alert_c set to 8.0, which syncs immediately. All seven
body fields are always present:
{
"file": "storage_alert.qo",
"body": {
"alert": "temp_excursion_high",
"temp_c": 9.2,
"lux": 0.12,
"door_open": false,
"door_open_sec": 0,
"time_valid": true,
"event_epoch": 1714435200
}
}event_epoch is the UTC epoch when the alert condition was first detected. It's
also preserved across retries, so a retried Note's body always carries the
authoritative original trigger time even though the Notecard envelope reflects
the retry time. time_valid is false if the Notecard RTC hadn't synced yet
when the alert fired. Downstream audit queries should treat those early-boot
alerts as commissioning data, not audit-grade records.
The firmware defines four alert types: temp_excursion_high,
temp_excursion_low, door_open_timeout, and sensor_disagreement. The last
one means the interior light is ON while the door switch reads CLOSED, which
points to a possible stuck-closed or failed-closed reed switch. It's only
meaningful on units with a door-actuated interior lamp (see
Limitations). Each type is rate-limited by
alert_cooldown_min to prevent alert fatigue during a sustained excursion.
Low-power strategy
A line-powered device sampling every 5 minutes doesn't strictly need aggressive power management. Keeping the host asleep the rest of the time still has two practical benefits. Less heat builds up inside the enclosure, which matters because a cold-storage monitor with meaningful self-heating is a calibration problem. And the firmware pattern ports cleanly to a battery-backed variant without a rewrite.
After each sample cycle, goToSleep() calls cxSleepUntilAttn() (in
cx_sleep.h). That issues card.attn with mode:sleep and the configured
sleep duration, waits for the Notecard to pull ATTN low, and puts the STM32
host into STOP2, where it draws a few microamps with its RAM intact. When the
interval is up, the Notecard raises ATTN, the rising edge on D6 wakes the
host, and loop() runs the next sample cycle. The host resumes in place instead
of cold-booting, so the AppState struct just lives in RAM and setup() only
runs once per power-up. If the sleep request fails or ATTN never goes low
(jumper missing), goToSleep() waits out the interval awake and tries again on
the next cycle, so the sample cadence holds either way. Sampling and
transmitting are deliberately decoupled. The firmware samples every 5 minutes
and enqueues one reading Note per wake, then flushes the accumulated queue in a
single cellular session on the 60-minute outbound cadence. Alerts are the only
thing that break that batch.
Power path and current expectations. On the deployed USB-C wall-power path (VUSB present), the Notecard's idle draw is higher than the µA-level figures published for VBAT-only operation, because the USB interface and monitoring circuits stay active while VUSB is asserted. What the ATTN-based sleep still buys you on USB-C is host MCU sleep. The STM32L433 sits in STOP2 (~1–2 µA) between wakes, which removes its contribution, and any self-heating from the host, during the 5-minute idle. The Notecard's own USB-C idle current is documented in the MBGLW DC characteristics table. The quantitative idle table in §9 applies only to the +VBAT bench configuration with VUSB absent. The host-active phase is estimated at 3–10 mA. No Blues factory specification exists for this combined phase, and this figure has not been validated against production hardware. Treat it as a commissioning target only, and measure the actual draw on your bench with a Mojo or current probe before you finalize any power budget.
Retry and error handling
- If any Notecard configuration step (
hub.set,note.template) is still unconfirmed after power-up, the firmware retries it at the start of each wake. It retries both transport failures (NULL response) and Notecard-reported errors, so a transient startup fault can't permanently skip the configuration step. fetchEnvOverrideschecks both the response pointer and theerrfield before applying values. A failed env fetch leaves the existing thresholds in place instead of reverting to defaults.- A sensor
NANreturn makes the reading Note carry a per-field sentinel. Atemp_cfault writes−9999, and aluxfault writes−1.0. Downstream parsers must check the right sentinel for each field.−9999intemp_cseparates a MAX31865/probe failure or fault from a legitimate near-zero temperature reading, while−1.0inluxis unambiguously a VEML7700 fault (lux can't be negative). alert_cooldown_min(default 30 minutes per alert type) de-duplicates alerts so a sustained excursion doesn't flood the on-call channel. The first alert fires within one sample cycle of the event, and re-alerts only fire after the cooldown period.
Key code snippet 1: Note.template definition (compression via fixed-length binary encoding)
The template encodes each reading as a compact fixed-length binary record, both
on the wire to Notehub and in on-device storage, which cuts per-Note overhead
compared to variable-length JSON. The template field codes are 14.1 for a
4-byte IEEE 754 float, 24 for a 4-byte unsigned int, and true for a boolean.
While the template is active, the Notecard encodes each storage_reading.qo
Note as fixed-length binary, and Notehub decodes it back to JSON for your
routes. NOTEFILE_READING is storage_reading.qo, defined in
cold_storage_audit_monitor_helpers.h. defineTemplates() returns true only
when the Notecard confirms the template without an error, and until it does,
loop() retries it on every wake and readings wait in the host-side ring
buffer.
// Field type tokens: 14.1 = 4-byte float, 24 = 4-byte uint, true = boolean.
bool defineTemplates() {
J *req = notecard.newRequest("note.template");
JAddStringToObject(req, "file", NOTEFILE_READING);
JAddNumberToObject(req, "port", 50);
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "temp_c", 14.1);
JAddNumberToObject(body, "lux", 14.1);
JAddNumberToObject(body, "door_open_sec", 24);
JAddBoolToObject(body, "door_open", true);
// UTC epoch at sample time — preserved across retries for audit lineage.
// 0 when time was not yet synced; pair with time_valid:false.
JAddNumberToObject(body, "sample_epoch", 24);
// false on samples taken before the Notecard RTC has synced with Notehub
JAddBoolToObject(body, "time_valid", true);
// Cumulative drop counters — observable in Notehub without a separate channel
JAddNumberToObject(body, "dropped_readings", 24);
JAddNumberToObject(body, "dropped_alerts", 24);
J *rsp = notecard.requestAndResponse(req);
// ...
}Key code snippet 2: immediate-sync alert
sync:true skips the scheduled outbound window. The Notecard opens a cellular
session within seconds of getting this request and delivers the Note to Notehub
before going back to idle. event_epoch carries the original trigger time, so
downstream audit queries see the authoritative timestamp even on a retried
send. This is sendAlert() in cold_storage_audit_monitor_helpers.cpp. A
sensor that returned NAN goes out as its sentinel (SENTINEL_NO_DATA, −9999,
for temp_c and SENTINEL_LUX_NO_DATA, −1, for lux). The function makes up
to three attempts, 250 milliseconds apart, and returns false if all three
fail, so the caller can stash the alert in the ring buffer.
for (int attempt = 0; attempt < 3; attempt++) {
if (attempt > 0) delay(250);
J *req = notecard.newRequest("note.add");
JAddStringToObject(req, "file", NOTEFILE_ALERT);
JAddBoolToObject(req, "sync", true);
J *body = JAddObjectToObject(req, "body");
JAddStringToObject(body, "alert", alert_type);
JAddNumberToObject(body, "temp_c",
(double)(isnan(temp_c) ? SENTINEL_NO_DATA : temp_c));
JAddNumberToObject(body, "lux",
(double)(isnan(lux) ? SENTINEL_LUX_NO_DATA : lux));
JAddBoolToObject(body, "door_open", door_open);
JAddNumberToObject(body, "door_open_sec", (int)door_open_sec);
JAddBoolToObject(body, "time_valid", time_valid);
// ...
JAddNumberToObject(body, "event_epoch", (double)event_epoch);
J *rsp = notecard.requestAndResponse(req);
// ...
}Key code snippet 3: sleeping until the Notecard raises ATTN
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 firmware checks the result of the
card.attn request instead of treating it as a fire-and-forget command. When
the ATTN rising edge brings the host back, the door-open timestamps, alert
cooldown state, and runtime configuration are all still in AppState. D5 is
the door switch, so cold_storage_audit_monitor_helpers.h sets CX_ATTN_PIN
to D6 instead of the cx_sleep.h default of D5. setup() calls
cxSleepBegin() once to arm the wake, and loop() ends every wake with
goToSleep(), shown here. log is the debug serial port, or NULL when
ENABLE_DEBUG is 0.
void goToSleep() {
// ...
if (!cxSleepUntilAttn(notecard, state.sample_interval_sec, NULL, log)) {
// The Notecard didn't take the sleep request, or ATTN never went low
// (check the ATTN -> D6 jumper). Keep the sample cadence and try again.
// ...
delay((unsigned long)state.sample_interval_sec * 1000UL);
}
}Key code snippet 4: light/door sensor disagreement
The VEML7700 gives an independent second opinion on door state. On a
door-actuated-lamp unit, an interior light that's on while the reed switch says
the door is closed points to a stuck-closed or failed-closed reed switch. The
switch contacts stay closed (D5 LOW) even though the interior lamp is lit. A
disconnected switch or displaced magnet would drive D5 HIGH (door is true)
and would NOT trigger this path. Those failure modes make the firmware believe
the door is open, which is the opposite condition. In this excerpt from
runSampleCycle(), the rule also waits for a synced clock (now > 0), skips
the check while an earlier sensor_disagreement alert is still waiting in the
retry ring, and honors alert_cooldown_min. If the send fails, the alert goes
into the ring for the next wake.
bool light_on = (!isnan(lux) && lux > state.lux_threshold);
if (light_on && !door && now > 0) {
if (!(state.pending_alert_type_mask & ALERT_BIT_SENSOR_DISAGREE) &&
(now - state.last_sensor_disagree_alert_time) >= state.alert_cooldown_sec) {
if (sendAlert("sensor_disagreement", temp_c, lux, false,
0, time_valid, now)) {
updateAlertCooldown("sensor_disagreement", now);
} else {
stashPendingAlert("sensor_disagreement", temp_c, lux,
false, 0, time_valid, now);
}
}
}8. Data Flow
Every sample_interval_sec (5 minutes by default), the host reads temperature,
lux, and door state, and four independent alert rules run against each sample:
temp_excursion_high: temperature abovetemp_high_alert_c(bench default 30.0°C; set it to 8.0°C in Notehub for USP 659 refrigerated storage). Fires on the first excursion reading and re-arms afteralert_cooldown_minif the temperature stays out of range.temp_excursion_low: temperature belowtemp_low_alert_c(bench default 15.0°C; set it to 2.0°C in Notehub for USP 659 refrigerated storage). Same cadence.door_open_timeout: the door has been continuously open for at leastdoor_open_alert_min(default 10 minutes). The firmware checks this by comparing the current epoch against the storeddoor_open_sincetimestamp.sensor_disagreement: interior light abovedoor_lux_thresholdwhile the reed switch reports closed, which indicates a possible stuck-closed or failed-closed switch state. This is a maintenance alert rather than a compliance excursion. It only means something on units with a door-actuated interior lamp, and it causes persistent false positives on always-on-lamp units unlessdoor_lux_thresholdis raised. See Limitations.
Collected. Each 5-minute wake records temperature (°C), ambient lux, a door-open boolean, the current door-open duration (seconds), and the UTC epoch.
Transmitted.
storage_reading.qo: one templated Note per wake (288 Notes/day by default at the 5-minute sample interval). Each Note carries the instantaneous temperature, ambient lux, door state, and how many seconds the door has been continuously open at reading time. Notes collect in the Notecard's flash-backed queue and flush in a batch on the scheduled 60-minute cellular outbound session. If a cellular outage spans several outbound windows, the queued Notes flush when connectivity returns, with their original UTC timestamps intact, up to the Notecard's on-device storage limit. Individual sample lineage is preserved for the depth of the Notecard queue across cellular and WiFi outages. An extended outage of several consecutive days may exhaust on-device storage and leave gaps in the record (see Limitations). This is distinct from the host-side retry ring. The firmware also keeps a 4-entry ring buffer for the case where the host can't reach the Notecard over I²C (e.g., a transient bus fault). If the host can't enqueue into the Notecard for more than four consecutive wakes, the oldest buffered reading is overwritten and counted indropped_readings. It is not preserved. Normal cellular outages never trigger this path. Only an I²C or Notecard-unreachable condition does.storage_alert.qo: one Note per rule trip (rate-limited byalert_cooldown_min), withsync:trueto open an immediate cellular session regardless of the scheduled outbound cadence.
Routed. Both Notefiles land in Notehub, where routes can send
storage_alert.qo to a paging or ticketing system in near-real time and
storage_reading.qo to a long-term compliance archive or LIMS system. The
Notefiles are distinct at the source, so the route configuration needs no
filtering logic, and each destination subscribes to exactly the volume it
needs.
9. Validation and Testing
Startup time-acquisition window. All alert logic (temperature excursion,
door timeout, sensor disagreement) and door-duration tracking wait for the
Notecard to return a valid UTC epoch from card.time (firmware guard:
now > 0). On first power-on, the Notecard has to register on the cellular
network and sync time with Notehub before card.time returns a non-zero value.
That usually takes one to several minutes, and it can take longer in
marginal-signal environments. During this initial window the device reads
sensors and enqueues reading Notes with unverified timestamps, but no
alerts fire. The Notecard's onboard RTC isn't synchronized until the first
Notehub session completes, so the when field on those early Notes may be
inaccurate. Treat Notes from this window as commissioning-only data, not
audit-grade records. Door timing in particular needs a valid epoch. If the door
is open before card.time returns non-zero, door_open_since isn't set and
door_open_sec in the reading Note will read zero. Duration tracking only
starts on the first sample where the door is seen open and the epoch is valid.
Plan for a commissioning warm-up period of at least 5 minutes before you rely on
alert delivery, door-duration accuracy, or audit-grade UTC timestamps in reading
Notes.
Expected steady-state behavior. A correctly functioning bench unit at room
temperature, with the compile-time default thresholds of 15 °C (low) and 30 °C
(high), generates one storage_reading.qo Note per 5-minute wake (288 per day
at the default interval) and zero storage_alert.qo Notes. Notes collect in the
Notecard queue and are delivered in a batch on each 60-minute cellular outbound
session. During commissioning, check that temp_c values in consecutive reading
Notes are stable and within the configured range. A −9999 in temp_c means a
MAX31865/probe fault or SPI failure on that sample, and a −1.0 in lux means
a VEML7700 fault. If a production Notehub fleet has already overridden
temp_high_alert_c to 8.0 and temp_low_alert_c to 2.0, the bench-mounted
exterior sensor will trip temp_excursion_high right away at room temperature.
That's expected, since the sensor isn't inside a refrigerated compartment.
Threshold smoke test. In the Notehub device environment variable UI, set
temp_high_alert_c slightly below the current actual temperature. The alert
condition is temp_c > temp_high_alert_c, so the threshold has to be below the
current reading for the condition to be met. If the sensor reads 22°C, for
example, set temp_high_alert_c to 20.0. The next inbound sync delivers the
new threshold, and the next sample cycle trips the alert. The
storage_alert.qo Note should show up in Notehub once a cellular session is
established after the alert (typically under 60 seconds). Restore the original
threshold and confirm that no more alerts fire after the cooldown period.
Door-event test. After the initial time-acquisition window, open the
cold-storage door and hold it open past door_open_alert_min. A
door_open_timeout alert Note should arrive in Notehub. Close the door and
confirm that the next storage_reading.qo Note shows door_open: false and
door_open_sec: 0. The Notes recorded while the door was open should show
door_open: true with climbing door_open_sec values.
Sensor-disagreement test. The rule fires only when
lux > door_lux_threshold AND the door switch reads CLOSED
(door_open = false, D5 LOW). Disconnecting the switch or removing the magnet
drives D5 HIGH (door_open = true), which does not meet the alert
condition, so don't use either one as the test stimulus. To trigger the alert
on a door-actuated-lamp unit, hold the door magnet directly against the reed
switch body while you open the door. The switch stays CLOSED (D5 LOW) while the
interior lamp comes on. You can also force D5 LOW externally (short D5 to GND
on the header to simulate a stuck-closed switch) while shining a flashlight at
the VEML7700. Either way, the firmware should emit a sensor_disagreement
alert on the next sample cycle.
Power validation with Mojo. The Mojo is a coulomb counter that reports cumulative mAh over its Qwiic connection. During bench validation, wire it inline on the Notecarrier CX +VBAT rail with a bench LiPo-range supply and no USB-C cable connected (see Wiring and Assembly for the complete bench setup). The figures below apply only to this +VBAT bench configuration with VUSB absent. They don't represent deployed USB-C operation. The Notecard idle and cellular-session figures come from the MBGLW datasheet and the Blues low-power firmware design guide. The host-active range is a bench-measured commissioning target, not a factory specification. Actual draw varies with signal quality, network registration time, and Note payload size:
| Phase | Expected current (+VBAT bench, VUSB absent) |
|---|---|
| Notecard idle (between samples; host in STOP2) | ~10–20 µA: Notecard in its deepest low-power state (~8–18 µA) plus the STM32L433 host in STOP2 (~1–2 µA) |
| Host active (sensor reads, ~2–4 seconds per wake) | ~3–10 mA estimated commissioning target (no factory specification; measure it on your bench). STM32L433 host running plus MAX31865 (SPI) and VEML7700 (I²C); Notecard radio off |
| Cellular session (alert or hourly sync) | 250 mA typical; up to 2000 mA peak during LTE Cat-1 bis transmit bursts, per the MBGLW datasheet VMODEM DC characteristics |
On the +VBAT bench setup (VUSB absent), a Mojo trace for a healthy 24-hour
period shows a quiet baseline of ~10–20 µA, broken by small milliamp blips every
5 minutes (host wake) and one larger burst per hour lasting 20–60 seconds
(cellular outbound session). If the baseline stays above ~1 mA, the host isn't
entering STOP2. Check the ATTN→D6 jumper and confirm the firmware was built
with the usb=none board option. If the hourly burst is missing, the Notecard
may not have registered on the network, so check the antenna connection and
hub.status in the blues.dev In-Browser Terminal.
On the deployed USB-C wall-power path (VUSB present), the Notecard's main supply comes through the USB-C rail, not through +VBAT. A Mojo wired inline on the +VBAT header pad while USB-C is connected is not inline with the actual supply path and won't measure deployed system current, so do not use Mojo on +VBAT when the USB-C supply is present. To measure total system current on a USB-C deployed unit, put an inline USB power meter (or a bench ammeter) in series with the USB-C supply lead. The Notecard's idle current on the VUSB rail is higher than the µA bench figures above. The MBGLW datasheet DC characteristics have the authoritative VUSB-powered idle figure. The quantitative table and the Mojo procedure above apply strictly to the +VBAT bench configuration with VUSB absent.
The MBGLW datasheet and the Notecard low-power firmware design guide have the complete, authoritative power figures.
10. Troubleshooting
Device does not appear in Notehub.
- Confirm that PRODUCT_UID is pasted correctly into
firmware/cold_storage_audit_monitor/cold_storage_audit_monitor.inoline 67 and that you've flashed the firmware. - Check that the SIM card in the Notecard has been activated. All Blues Notecards ship with an active SIM, which you can confirm under your account in the Blues Shop.
- Check the antenna connection. The SMA-to-u.FL adapter must be firmly seated on the CELL u.FL port (not the GPS port). With no external antenna, or with the antenna inside a metal enclosure, the Notecard may not register on the network.
- Open the Notehub Project → Devices tab and look for the device serial number. If it's in the device list but shows no Events, check the cellular signal at that location, since weak signal can delay first registration.
Temperature readings are −9999 or missing.
- The MAX31865 amplifier or PT1000 probe has a fault. Check the SPI wiring: CLK, SDI (MISO label on CX), SDO (MOSI label on CX), CS (D10), +3.3V, GND. Watch out for the Notecarrier CX label swap. The silkscreen labels MOSI and MISO are reversed, so follow the pin table in §5, not the labels.
- If you're using a bench TMP117 instead, confirm the Qwiic cable is connected and that you've swapped the library from MAX31865 to SparkFun_TMP117 (see §4).
- Open the Notehub Project → Terminal tab, select your device, and run
card.statusto see whether the Notecard is reporting a fault condition.
Lux readings are −1.0.
- The VEML7700 sensor isn't responding on the I²C bus. Make sure the Qwiic cable runs from the Notecarrier CX Qwiic connector to the VEML7700 Qwiic IN connector and isn't kinked or damaged.
- Make sure there's no TMP117 in the production I²C chain. The production design connects the VEML7700 directly to the Notecarrier CX Qwiic port.
No alerts are firing (or alerts fire when they shouldn't).
- Confirm that the Notecard's RTC has synced with Notehub. In the Notehub
Events tab, the first few reading Notes may show
time_valid: false, and no alerts fire during that pre-sync window. Wait at least 5 minutes after power-on, then test. - For temperature alerts, check that you've overridden
temp_high_alert_candtemp_low_alert_cin Notehub Fleet → Environment. With the bench defaults (30°C high, 15°C low), a refrigerated unit at 4°C firestemp_excursion_lowevery cooldown period, and a warm excursion never firestemp_excursion_highuntil it passes 30°C. - For door alerts, confirm the reed switch is wired to D5 with a pull-up
enabled. Open the door and hold it open longer than
door_open_alert_min(default 10 minutes), then check Notehub → Events for astorage_alert.qoNote withalert: "door_open_timeout".
The device is consuming too much power.
- Check that the host is entering STOP2. On a +VBAT bench setup (no USB-C), the
baseline current should be ~10–20 µA between samples. If the baseline is
persistently > 1 mA, either the
ATTN→D6jumper is missing (so the firmware waits out each interval awake) or the firmware was built with USB support enabled. With the USB CDC stack on and no USB host attached, the host leaves STOP2 within a millisecond. Confirm the jumper and theusb=nonebuild option (see §7). - On a USB-C powered deployment, the Notecard's idle current is higher than the µA bench figures (see §9 Power validation), and that's expected.
11. Limitations and Next Steps
This reference design produces the measurement record and the cellular data path an auditor cares about. Several of the surrounding pieces of a regulated cold-chain program are explicitly out of scope, though: the calibration certificate that goes with each probe, the SOPs that govern the program, the multi-point mapping a large freezer needs, and the ultra-cold (−80°C) hardware path. The list below spells out those boundaries so anyone evaluating this design against a real compliance program can see exactly what they still need to bring.
Deployment Considerations
These are the parts of a regulated cold-chain program that this design deliberately leaves to the operator. Check each one against your own compliance program to see what you'll need to supply.
Regulatory and compliance scope. This reference design produces an audit-evidence record of temperature readings, door events, and excursion alerts, and it sends that record to Notehub over an independent cellular data path. It does not implement, and does not substitute for, site validation, SOP authorship, calibration program management, record-retention policy, electronic-record controls (e.g., 21 CFR Part 11 audit trails, access controls, and change management), or the broader quality-management framework that any specific regulatory body requires. Deploying this design as part of a monitored, compliant storage program requires an exact calibrated probe assembly with a current NIST-traceable certificate, written commissioning and operating procedures, and validation documentation. Those are operator responsibilities, not firmware features.
The production probe assembly is specified; obtain its NIST-traceable calibration certificate before deploying. The firmware implements the Adafruit Platinum RTD Sensor PT1000 3-Wire 1 m (Product 3984) through the Adafruit MAX31865 PT1000 Amplifier (Product 3648). The PT1000 probe's 316L stainless-steel capsule routes into the refrigerated compartment, and the MAX31865 amplifier board mounts inside the enclosure. This path provides the required cable-mounted, compartment-internal temperature measurement. The probe does not ship with a NIST-traceable calibration certificate. Before regulatory deployment, submit the specific probe assembly to an accredited calibration laboratory (e.g., Transcat, Tektronix Calibration, or a lab accredited under ILAC to ISO/IEC 17025) to get a calibration certificate traceable to NIST for that individual unit. Keep the certificate on file with the unit's commissioning and IQ/OQ documentation, and put the unit on a recertification schedule that matches your calibration management program. The SparkFun TMP117 breakout (SEN-15805) is a bench substitute only. §4 Hardware Requirements describes the library swap it needs.
Single-point temperature measurement. One PT1000 probe measures a single
location in the compartment. USP Chapter 659 and CDC guidelines recommend
placing the sensor at the geometric center of the unit, away from vents and
walls. Units with high thermal gradients (e.g., large reach-in freezers) may
need multiple probes. Adding a second MAX31865 on another SPI chip-select pin
(e.g., D9) with a second calibrated PT1000 probe, plus a second temp_c_2
template field, is a straightforward extension.
No cryptographic Note signing. Notecard includes an STSAFE secure element.
This design doesn't implement note-level signing, though. The received
timestamps assigned by Notehub are server-side and can't be altered after
delivery, but the Note body and the when timestamp the Notecard sets aren't
signed on the device. On-device signing is out of scope
for this reference design.
Alerting and door-duration tracking begin only after time is acquired. All
alert rules and door-duration timestamps wait for the Notecard to return a valid
UTC epoch (now > 0). Initial cellular registration and time sync usually take a
few minutes on first power-on and can take longer where signal is marginal.
During that time the device samples sensors and enqueues reading Notes, but it
does not fire alerts or start the door-duration timer. If the door is open
during this pre-time window, the firmware waits until a valid epoch is
available before setting door_open_since, and door_open_sec in those early
reading Notes reads zero until time is valid and the door has been seen open
with a known timestamp. Commission each unit in a known-good environment and
verify alert delivery before treating the unit as fully operational. Reading
Notes enqueued during this pre-sync window carry unverified Notecard RTC
timestamps and should be flagged as commissioning data rather than
audit-grade records in any downstream export.
Door-open duration tracking is sampled, not interrupt-driven. A door that
opens and closes entirely within one sample_interval_sec window won't be
detected. At the default 5-minute interval, any access shorter than 5 minutes is
invisible. For pharmacies with high access frequency (every few minutes),
shortening sample_interval_sec to 60–120 seconds shrinks the blind spot at the
cost of more frequent host wakes.
sensor_disagreement requires a door-actuated interior lamp. The
light-based cross-check (ambient lux above door_lux_threshold while the reed
switch reads closed) only catches a stuck-closed or failed-closed reed
switch on units where the interior lamp turns on when the cabinet opens and
off when it closes. On lamp-free units, lux never exceeds the threshold from
an interior light event, so the rule never fires. Omit the VEML7700 or set
door_lux_threshold to 120000.0 in Notehub. On always-on-lamp units (the
lamp stays lit regardless of door state), lux > door_lux_threshold is true
whenever the door is closed (the normal closed-door state). The rule then fires
persistently every cooldown period, which means continuous false positives
rather than silence. Set door_lux_threshold to 120000.0 in Notehub to
suppress the rule on always-on-lamp units without a firmware rebuild. Verify the
lamp behavior of the specific unit before you deploy this rule.
VEML7700 placement is bench-only. The bare Adafruit VEML7700 breakout PCB
isn't rated for sustained cold or condensing environments. Permanently mounting
the unprotected PCB inside a refrigerated cabinet will eventually cause
moisture-related corrosion and failure. That's the same risk behind this
document's advice to keep all other electronics outside the cold zone. For a
production design, use a sealed or potted light sensor rated for the operating
temperature range, mount the sensor on the outside of the door frame where it
detects light spilling out when the door is ajar, or use a different secondary
door-verification signal (such as a suitably packaged hall-effect sensor).
Production builds can leave out the sensor_disagreement feature where no
reliable cold-tolerant light-sensing solution has been specified.
Two independent buffering layers with different depth and failure-mode
guarantees protect the audit record, and for compliance purposes you need to
keep them clearly separate. The first is the Notecard flash-backed queue,
which handles cellular and WiFi outages transparently. Notes collect in the
Notecard's on-device flash store and flush when connectivity returns, with UTC
timestamps intact. At the default 5-minute sample interval this design enqueues
288 Notes per day, and an extended cellular outage of several consecutive days
can exhaust on-device flash storage, leaving gaps in the audit-evidence record.
Using a note.template for storage_reading.qo encodes each Note as a compact
fixed-length binary record, which reduces per-Note storage overhead and improves
wire efficiency compared to variable-length JSON. On-device queue depth is still
finite, though, and bounded by the Notecard's total flash capacity. Configure a
shorter outbound interval to keep the queue shallow under normal operation.
The second layer is the host-side I²C retry ring (PENDING_RING_CAP = 4),
which only handles the separate case where the STM32 host can't enqueue into the
Notecard over I²C (e.g., a transient bus fault or Notecard startup delay). This
ring holds at most four undelivered readings and four undelivered alerts. If the
host can't reach the Notecard for more than four consecutive wakes (~20 minutes
at the default interval), the oldest buffered entry is overwritten and its loss
is counted in dropped_readings or dropped_alerts. The payload is not
preserved beyond that window. Normal cellular outages never trigger this path.
For a compliance use case where full sample lineage through an extended
I²C-fault window is a hard requirement, add a larger host-side spill buffer
(FRAM, SD card, or SPI flash) on the host's SPI bus.
No power-fail event. If the facility loses power and the monitor isn't on a UPS, both the Notecard and the host go down. Any Notes that were queued but not yet synced stay in the Notecard's flash-backed queue and are delivered on the next power-on and cellular session. A gap in the reading series will still show up in Notehub, which is itself a useful signal for the audit-evidence record. Adding a small LiFePO4 backup cell on the +VBAT rail would let the monitor ride through brief outages and log the power-loss event explicitly.
Mojo is bench-only. The firmware doesn't read the Mojo's LTC2959 coulomb counter over the Qwiic bus. If fleet-level energy telemetry would be valuable, adding a runtime mAh field to the reading Note is a straightforward extension.
Production Next Steps
Once the basic audit monitor is producing records an auditor trusts, these extensions come next. They run from multi-point coverage through fleet maintenance to the ultra-cold hardware path.
A second MAX31865 + PT1000 probe on a different chip-select (e.g., D9)
allows multi-point mapping of larger units. The template and per-sample reading
logic extend easily: add a temp_c_2 field to the template and read both
sensors on each wake. Each probe assembly needs its own NIST-traceable
calibration certificate.
Notecard Outboard DFU provides over-the-air host firmware updates, which you'll need to keep a fleet current through regulatory-threshold changes without site visits.
A storage_calibration.db inbound Notefile delivers per-unit
temperature-offset corrections from Notehub, so you don't have to reflash when
calibration data changes after annual re-certification.
Notehub-side webhook routing connects the device to a LIMS or compliance database. The per-sample reading Note body maps directly to a time-series schema, and the alert Note maps to an excursion event record.
Ultra-cold storage (−80°C freezers) is not supported by this hardware. The Adafruit PT1000 probe (Product 3984) is rated to −50°C. Using it below that limit is outside the manufacturer's specification and is not appropriate for a regulatory-grade deployment. Ultra-cold monitoring requires a dedicated RTD probe and amplifier chain rated to −80°C or below (e.g., a PT100 probe specified for cryogenic service), a calibration certificate that covers that lower temperature range, and validation of the full measurement chain at operating temperature. The firmware architecture (per-sample reading Notes, immediate-sync alert Notes, environment-variable thresholds) is compatible with that extension, but the sensor hardware must be replaced.
12. Summary
Most pharmacy cold-chain incidents start with an undocumented excursion that nobody noticed, or a weekend door-open event that no one was around to catch, rather than with a failed sensor. The compliance officer who used to rely on once-per-shift wall-thermometer readings now has a continuous, individually timestamped record beside every unit in scope. It comes from a PT1000 probe inside the compartment (with its own NIST-traceable calibration certificate before deployment), a reed switch on the door, a light sensor as the independent second opinion, and a cellular uplink that bypasses the pharmacy's regulated network entirely. Reading Notes accumulate locally through network outages and sync once cellular returns, so a WiFi failure doesn't break the audit record. A power loss or a multi-day outage can still leave gaps (see Limitations). In an audit, that independent cellular path is the difference between a complete record and a "we think it was fine" conversation, which makes it far more than a convenience. The same pattern scales from a single pharmacy refrigerator to a fleet across dozens of sites, with thresholds per storage class, routes per destination, environment variables per device, and no reflash required. Ultra-cold (−80°C) storage needs a probe rated for that lower temperature and is outside the scope of this reference design.
On This Page
- 1. Project Overview
- 2. System Architecture
- 3. Technical Summary
- 4. Hardware Requirements
- 5. Wiring and Assembly
- 6. Notehub Setup
- 7. Firmware Design
- Modules
- Sensor reading strategy
- Event payload design
- Low-power strategy
- Retry and error handling
- Key code snippet 1: Note.template definition (compression via fixed-length binary encoding)
- Key code snippet 2: immediate-sync alert
- Key code snippet 3: sleeping until the Notecard raises ATTN
- Key code snippet 4: light/door sensor disagreement
- 8. Data Flow
- 9. Validation and Testing
- 10. Troubleshooting
- 11. Limitations and Next Steps
- 12. Summary